Metode rantai vs enkapsulasi

17

Ada masalah OOP klasik dari metode chaining vs metode "single-access-point":

main.getA().getB().getC().transmogrify(x, y)

vs.

main.getA().transmogrifyMyC(x, y)

Yang pertama tampaknya memiliki keuntungan bahwa masing-masing kelas hanya bertanggung jawab untuk satu set operasi yang lebih kecil, dan membuat semuanya lebih modular - menambahkan metode ke C tidak memerlukan upaya apa pun dalam A, B atau C untuk mengeksposnya.

Kelemahannya, tentu saja, adalah enkapsulasi yang lebih lemah , yang dipecahkan oleh kode kedua. Sekarang A memiliki kendali atas setiap metode yang melewatinya, dan dapat mendelegasikannya ke bidangnya jika diinginkan.

Saya menyadari tidak ada solusi tunggal dan tentu saja tergantung pada konteks, tetapi saya benar-benar ingin mendengar beberapa masukan tentang perbedaan penting lainnya antara kedua gaya, dan dalam keadaan apa saya harus memilih keduanya - karena saat ini, ketika saya mencoba untuk merancang beberapa kode, saya merasa seperti saya tidak menggunakan argumen untuk memutuskan satu atau lain cara.

ek
sumber

Jawaban:

25

Saya pikir Hukum Demeter memberikan pedoman penting dalam hal ini (dengan kelebihan dan kekurangannya, yang, seperti biasa, harus diukur berdasarkan kasus per kasus).

Keuntungan mengikuti Hukum Demeter adalah bahwa perangkat lunak yang dihasilkan cenderung lebih mudah dirawat dan beradaptasi. Karena objek kurang tergantung pada struktur internal objek lain, wadah objek dapat diubah tanpa mengerjakan ulang peneleponnya.

Kelemahan dari Hukum Demeter adalah kadang-kadang membutuhkan penulisan sejumlah besar metode "pembungkus" kecil untuk menyebarkan panggilan metode ke komponen. Selain itu, antarmuka kelas dapat menjadi besar karena host metode untuk kelas yang terkandung, menghasilkan kelas tanpa antarmuka yang kohesif. Tapi ini juga bisa menjadi tanda desain OO yang buruk.

Péter Török
sumber
Saya lupa tentang hukum itu, terima kasih telah mengingatkan saya. Tapi yang saya tanyakan di sini adalah apa kelebihan dan kekurangannya, atau lebih tepatnya bagaimana saya harus memutuskan untuk menggunakan satu gaya di atas yang lain.
Oak
@ Oak, saya menambahkan kutipan yang menjelaskan kelebihan dan kekurangan.
Péter Török
10

Secara umum saya mencoba untuk menjaga metode chaining sebatas mungkin (berdasarkan Hukum Demeter )

Satu-satunya pengecualian yang saya buat adalah untuk antarmuka yang lancar / pemrograman gaya DSL internal.

Martin Fowler membuat semacam perbedaan yang sama dalam Domain-Bahasa-Khusus tetapi karena alasan pemisahan permintaan perintah pelanggaran yang menyatakan:

bahwa setiap metode harus berupa perintah yang melakukan tindakan, atau permintaan yang mengembalikan data ke pemanggil, tetapi tidak keduanya.

Fowler dalam bukunya di halaman 70 mengatakan:

Pemisahan command-query adalah prinsip yang sangat berharga dalam pemrograman, dan saya sangat mendorong tim untuk menggunakannya. Salah satu konsekuensi dari menggunakan Metode Chaining di DSLs internal adalah bahwa ia biasanya melanggar prinsip ini - setiap metode mengubah keadaan tetapi mengembalikan objek untuk melanjutkan rantai. Saya telah menggunakan banyak desibel yang meremehkan orang-orang yang tidak mengikuti pemisahan permintaan-perintah, dan akan melakukannya lagi. Tetapi antarmuka yang lancar mengikuti serangkaian aturan yang berbeda, jadi saya senang untuk mengizinkannya di sini.

KeesDijk
sumber
3

Saya pikir pertanyaannya adalah, apakah Anda menggunakan abstraksi yang sesuai.

Dalam kasus pertama, kita punya

interface IHasGetA {
    IHasGetB getA();
}

interface IHasGetB {
    IHasGetC getB();
}

interface IHasGetC {
    ITransmogrifyable getC();
}

interface ITransmogrifyable {
    void transmogrify(x,y);
}

Di mana utama adalah tipe IHasGetA. Pertanyaannya adalah: Apakah abstraksi itu cocok? Jawabannya tidak sepele. Dan dalam hal ini terlihat sedikit aneh, tapi ini contoh teoretis. Tetapi untuk membangun contoh yang berbeda:

main.getA(v).getB(w).getC(x).transmogrify(y, z);

Seringkali lebih baik daripada

main.superTransmogrify(v, w, x, y, z);

Karena dalam contoh terakhir kedua thisdan maintergantung pada jenis v, w, x, ydan z. Juga, kode tidak benar-benar terlihat jauh lebih baik, jika setiap deklarasi metode memiliki setengah lusin argumen.

Seorang pencari lokasi sebenarnya membutuhkan pendekatan pertama. Anda tidak ingin mengakses contoh yang dibuatnya melalui pencari lokasi layanan.

Jadi "menjangkau" melalui suatu objek dapat menciptakan banyak ketergantungan, terlebih lagi jika didasarkan pada properti dari kelas aktual.
Namun menciptakan abstraksi, itu semua tentang menyediakan objek adalah hal yang sama sekali berbeda.

Misalnya, Anda dapat memiliki:

class Main implements IHasGetA, IHasGetA, IHasGetA, ITransmogrifyable {
    IHasGetB getA() { return this; }
    IHasGetC getB() { return this; }
    ITransmogrifyable getC() { return this; }
    void transmogrify(x,y) {
        return x + y;//yeah!
    }
}

Di mana mainadalah contoh dari Main. Jika pengetahuan kelas mainmengurangi ketergantungan IHasGetAbukan Main, Anda akan menemukan kopling menjadi sangat rendah. Kode panggilan bahkan tidak tahu itu sebenarnya memanggil metode terakhir pada objek asli, yang sebenarnya menggambarkan tingkat decoupling.
Anda mencapai sepanjang jalan abstraksi singkat dan ortogonal, daripada jauh ke internal implementasi.

back2dos
sumber
Poin yang sangat menarik tentang peningkatan besar dalam jumlah parameter.
Oak
2

The Law of Demeter , seperti @ Péter Török menunjukkan, menunjukkan "kompak" bentuk.

Selain itu, semakin banyak metode yang Anda sebutkan secara eksplisit dalam kode Anda, semakin banyak kelas yang bergantung pada kelas Anda, meningkatkan masalah pemeliharaan. Dalam contoh Anda, bentuk kompak tergantung pada dua kelas, sedangkan bentuk yang lebih panjang tergantung pada empat kelas. Bentuk yang lebih panjang tidak hanya bertentangan dengan Hukum Demeter; itu juga akan membuat Anda mengubah kode Anda setiap kali Anda mengubah salah satu dari empat metode yang direferensikan (sebagai lawan dua dalam bentuk kompak).

CesarGon
sumber
Di sisi lain dengan membabi buta mengikuti undang-undang itu berarti bahwa jumlah metode Atelah akan meledak dengan banyak metode yang Amungkin ingin didelegasikan pergi. Namun, saya setuju dengan dependensi - yang secara drastis mengurangi jumlah dependensi yang diperlukan dari kode klien.
Oak
1
@ Oak: Melakukan sesuatu secara membabi buta tidak pernah baik. Orang perlu melihat pro dan kontra dan membuat keputusan berdasarkan bukti. Ini termasuk Hukum Demeter, juga.
CesarGon
2

Saya sendiri telah bergulat dengan masalah ini. Kelemahan dengan "mencapai" jauh ke objek yang berbeda adalah bahwa ketika Anda melakukan refactoring Anda akhirnya harus mengubah banyak kode karena ada begitu banyak dependensi. Selain itu, kode Anda menjadi sedikit membengkak dan sulit dibaca.

Di sisi lain, memiliki kelas yang hanya "meneruskan" metode juga berarti overhead harus mendeklarasikan beberapa metode di lebih dari satu tempat.

Solusi yang mengurangi ini dan tepat dalam beberapa kasus adalah memiliki kelas pabrik yang membangun semacam objek fasad dengan menyalin data / objek dari kelas yang sesuai. Dengan begitu Anda dapat mengkodekan objek fasad Anda dan ketika Anda refactor Anda cukup mengubah logika pabrik.

Homde
sumber
1

Saya sering menemukan bahwa logika suatu program lebih mudah untuk grok dengan metode berantai. Bagi saya, customer.getLastInvoice().itemCount()cocok dengan otak saya lebih baik daripada customer.countLastInvoiceItems().

Apakah itu sepadan dengan sakit kepala pemeliharaan karena memiliki kopling tambahan terserah Anda. (Saya juga suka fungsi kecil di kelas kecil, jadi saya cenderung berantai. Saya tidak mengatakan itu benar - itu hanya apa yang saya lakukan.)

JT Grimes
sumber
yang harus menjadi pelanggan IMO.NrLastInvoices atau pelanggan.LastInvoice.NrItems. Rantai itu tidak terlalu panjang sehingga mungkin tidak layak untuk diratakan jika jumlah kombinasinya agak besar
Homde