Prinsip ilmu paling sedikit

32

Saya memahami motif di balik prinsip paling sedikit pengetahuan , tetapi saya menemukan beberapa kelemahan jika saya mencoba menerapkannya dalam desain saya.

Salah satu contoh dari prinsip ini (sebenarnya bagaimana tidak menggunakannya), yang saya temukan dalam buku Head First Design Patterns menentukan bahwa salah memanggil metode pada objek yang dikembalikan dari memanggil metode lain, dalam hal prinsip ini .

Namun sepertinya terkadang sangat diperlukan untuk menggunakan kemampuan tersebut.

Sebagai contoh: Saya memiliki beberapa kelas: kelas video-capture, kelas encoder, kelas streamer, dan mereka semua menggunakan beberapa kelas dasar lainnya, VideoFrame, dan karena mereka berinteraksi satu sama lain, mereka dapat melakukan misalnya sesuatu seperti ini:

streamer kode kelas

...
frame = encoder->WaitEncoderFrame()
frame->DoOrGetSomething();
....

Seperti yang Anda lihat, prinsip ini tidak diterapkan di sini. Dapatkah prinsip ini diterapkan di sini, atau apakah prinsip ini tidak selalu dapat diterapkan dalam desain seperti ini?

tebusan
sumber
kemungkinan duplikat dari Metode chaining vs enkapsulasi
agas
4
Itu mungkin duplikat yang lebih dekat.
Robert Harvey
1
Terkait: Apakah kode seperti ini "kecelakaan kereta api" (melanggar Hukum Demeter)? (pengungkapan penuh: itu pertanyaan saya sendiri)
CVn

Jawaban:

21

Prinsip yang Anda bicarakan (lebih dikenal sebagai Law of Demeter ) untuk fungsi dapat diterapkan dengan menambahkan metode pembantu lain ke kelas streamer Anda seperti

  {
    frame = encoder->WaitEncoderFrame()
    DoOrGetSomethingForFrame(frame); 
    ...
  }

  void DoOrGetSomethingForFrame(Frame *frame)
  {
     frame->DoOrGetSomething();
  }  

Sekarang, setiap fungsi hanya "berbicara dengan teman", bukan dengan "teman teman".

IMHO ini adalah pedoman kasar yang dapat membantu menciptakan metode yang mengikuti prinsip tanggung jawab tunggal dengan lebih ketat. Dalam kasus sederhana seperti yang di atas mungkin sangat disarankan jika ini benar-benar layak kerumitan dan jika kode yang dihasilkan benar-benar "bersih", atau jika itu hanya akan memperluas kode Anda secara formal tanpa keuntungan yang nyata.

Doc Brown
sumber
Pendekatan ini memang memiliki keuntungan membuatnya lebih mudah untuk unit test, debug, dan alasan tentang kode Anda.
gntskn
+1. Meskipun, ada alasan yang sangat bagus untuk tidak membuat perubahan kode adalah bahwa antarmuka kelas mungkin menjadi membengkak dengan metode yang tugasnya hanya membuat satu atau beberapa panggilan menjadi beberapa metode lain (dan tanpa logika tambahan). Dalam beberapa jenis lingkungan pemrograman, katakanlah C ++ COM (terutama WIC dan DirectX), ada biaya tinggi yang terkait dengan penambahan setiap metode tunggal ke antarmuka COM, dibandingkan dengan bahasa lain.
rwong
1
Dalam desain antarmuka C ++ COM, mendekomposisi kelas besar menjadi kelas kecil (yang berarti Anda memiliki kesempatan lebih tinggi untuk berbicara dengan banyak objek) dan meminimalkan jumlah metode antarmuka adalah dua tujuan desain dengan manfaat nyata (pengurangan biaya) berdasarkan pemahaman mendalam mekanika internal (tabel virtual, penggunaan kembali kode, banyak hal lainnya). Oleh karena itu programmer C ++ COM biasanya harus mengabaikan LoD.
rwong
10
+1: Hukum Demeter benar-benar harus dinamai Saran dari Demeter: YMMV
Binary Worrier
Hukum demeter adalah saran untuk membuat pilihan arsitektur, sementara Anda menyarankan perubahan kosmetik sehingga tampaknya Anda mematuhi 'hukum' itu. Itu akan menjadi kesalahpahaman, karena perubahan sintaksis tidak berarti tiba-tiba semuanya baik-baik saja. Sejauh yang saya tahu hukum demeter pada dasarnya berarti: jika Anda ingin melakukan OOP, maka berhentilah menulis kode prodecural dengan fungsi pengambil di mana-mana.
user2180613
39

Prinsip ilmu paling tidak atau Hukum Demeter adalah peringatan agar tidak melibatkan kelas Anda dengan detail kelas lain yang melintasi lapisan demi lapisan. Ini memberi tahu Anda bahwa lebih baik berbicara hanya dengan "teman" Anda dan bukan dengan "teman teman".

Bayangkan Anda telah diminta untuk mengelas perisai ke patung ksatria berbaju zirah. Anda dengan hati-hati memposisikan perisai di lengan kiri sehingga terlihat alami. Anda perhatikan ada tiga tempat kecil di lengan bawah, siku, dan lengan atas tempat perisai menyentuh armor. Anda mengelas ketiga tempat karena Anda ingin memastikan koneksi kuat. Sekarang bayangkan bos Anda marah karena dia tidak bisa menggerakkan siku bajunya. Anda mengira baju besi itu tidak akan pernah bergerak dan karena itu menciptakan koneksi tidak bergerak antara lengan bawah dan lengan atas. Perisai seharusnya hanya terhubung ke temannya, lengan. Bukan untuk mempersenjatai teman. Bahkan jika Anda harus menambahkan sepotong logam untuk membuatnya tersentuh.

Metafora itu bagus tapi apa yang sebenarnya kita maksud dengan teman? Apa pun yang diketahui cara membuat atau menemukan objek adalah teman. Atau, suatu objek hanya bisa meminta untuk menyerahkan objek lain , yang hanya mengetahui antarmuka. Ini tidak dianggap sebagai teman karena tidak ada harapan tentang cara mendapatkannya. Jika objek tidak tahu dari mana asalnya karena ada sesuatu yang lewat / disuntikkan maka itu bukan teman teman, itu bahkan bukan teman. Ini adalah sesuatu yang hanya diketahui oleh objek yang digunakan. Itu hal yang baik.

Ketika mencoba menerapkan prinsip-prinsip seperti ini, penting untuk dipahami bahwa prinsip-prinsip itu tidak pernah melarang Anda mencapai sesuatu. Mereka adalah peringatan bahwa Anda mungkin lalai untuk melakukan lebih banyak pekerjaan untuk mencapai desain yang lebih baik yang mencapai hal yang sama.

Tidak ada yang ingin melakukan pekerjaan tanpa alasan, jadi penting untuk memahami apa yang Anda dapatkan dari mengikuti ini. Dalam hal ini membuat kode Anda fleksibel. Anda dapat membuat perubahan dan memiliki sedikit kelas lain yang terpengaruh oleh perubahan yang perlu dikhawatirkan. Itu kedengarannya bagus tetapi tidak membantu Anda memutuskan apa yang harus dilakukan kecuali Anda menganggapnya sebagai semacam doktrin agama.

Alih-alih secara membabi buta mengikuti prinsip ini, ambillah versi sederhana dari masalah ini. Tulis solusi yang tidak mengikuti prinsip dan yang tidak. Sekarang setelah Anda memiliki dua solusi, Anda dapat membandingkan seberapa reseptif masing-masing terhadap perubahan dengan mencoba membuatnya di keduanya.

Jika Anda TIDAK BISA menyelesaikan masalah saat mengikuti prinsip ini, ada kemungkinan Anda kehilangan keahlian lain.

Salah satu solusi untuk masalah khusus Anda adalah dengan menyuntikkan framesesuatu (kelas atau metode) yang tahu cara berbicara dengan bingkai sehingga Anda tidak harus menyebarkan semua detail frame chatting di kelas Anda, yang sekarang hanya tahu bagaimana dan kapan harus dapatkan bingkai.

Itu sebenarnya mengikuti prinsip lain: Pisahkan penggunaan dari konstruksi.

frame = encoder->WaitEncoderFrame()

Dengan menggunakan kode ini, Anda bertanggung jawab untuk memperoleh suatu Frame. Anda belum memikul tanggung jawab untuk berbicara dengan seorang Frame.

frame->DoOrGetSomething(); 

Sekarang Anda harus tahu cara berbicara dengan Frame, tetapi gantilah dengan:

new FrameHandler(frame)->DoOrGetSomething();

Dan sekarang Anda hanya perlu tahu cara berbicara dengan teman Anda, FrameHandler.

Ada banyak cara untuk mencapai ini dan ini, mungkin, bukan yang terbaik tetapi itu menunjukkan bagaimana mengikuti prinsip tidak membuat masalah tidak dapat diselesaikan. Itu hanya menuntut lebih banyak pekerjaan dari Anda.

Setiap aturan yang baik memiliki pengecualian. Contoh terbaik yang saya tahu adalah Bahasa Spesifik Domain internal . Sebuah DSL s rantai metodetampaknya menyalahgunakan Hukum Demeter sepanjang waktu karena Anda terus-menerus memanggil metode yang mengembalikan berbagai jenis dan menggunakannya secara langsung. Kenapa ini oke? Karena dalam DSL semua yang dikembalikan adalah teman yang dirancang dengan hati-hati sehingga Anda harus berbicara langsung. Sesuai desain, Anda telah diberi hak untuk mengharapkan rantai metode DSL tidak berubah. Anda tidak memiliki hak itu jika Anda hanya secara acak mempelajari rantai basis kode bersama-sama apa pun yang Anda temukan. DSL terbaik adalah representasi atau antarmuka yang sangat tipis ke objek lain yang kemungkinan besar tidak perlu Anda selidiki. Saya hanya menyebutkan ini karena saya menemukan saya memahami hukum demeter jauh lebih baik setelah saya mengetahui mengapa DSL adalah desain yang baik. Beberapa orang bahkan mengatakan DSL tidak melanggar hukum demeter yang sebenarnya.

Solusi lain adalah membiarkan sesuatu yang lain menyuntikkan frameke Anda. Jika framedatang dari setter atau lebih disukai seorang konstruktor maka Anda tidak bertanggung jawab untuk membangun atau mendapatkan bingkai. Ini berarti Anda berperan di sini lebih seperti FrameHandlersyang akan terjadi. Alih-alih sekarang kaulah yang suka mengobrol Framedan membuat sesuatu yang lain mencari cara untuk mendapatkan. Frame Dengan cara ini adalah solusi yang sama dengan perubahan perspektif sederhana.

The SOLID prinsip adalah orang-orang besar saya mencoba mengikuti. Dua yang dihormati di sini adalah Prinsip Tanggung Jawab Tunggal dan Pembalikan Ketergantungan. Sangat sulit untuk menghormati kedua orang ini dan akhirnya melanggar Hukum Demeter.

Mentalitas yang masuk ke dalam pelanggaran Demeter adalah seperti makan di restoran prasmanan di mana Anda hanya mengambil apa pun yang Anda inginkan. Dengan sedikit kerja dimuka Anda dapat menyediakan diri Anda dengan menu dan server yang akan membawakan apa pun yang Anda suka. Duduk, santai, dan tip dengan baik.

candied_orange
sumber
2
"Ini memberitahu Anda bahwa lebih baik berbicara hanya dengan" teman "dan bukan dengan" teman teman "" ++
RubberDuck
1
Paragraf kedua, apakah itu dicuri dari suatu tempat, atau apakah Anda mengada-ada? Itu membuat saya mengerti konsep sepenuhnya . Itu membuatnya terang-terangan tentang apa itu "teman teman", dan apa kekurangannya untuk melibatkan terlalu banyak kelas (sendi). Kutipan +.
die maus
2
@diemaus Jika saya mencuri paragraf pelindung dari suatu tempat sumbernya telah bocor keluar dari kepala saya. Saat itu otak saya hanya berpikir itu pintar. Apa yang saya ingat adalah bahwa saya menambahkannya setelah banyak upvotes jadi senang melihatnya divalidasi. Senang itu membantu.
candied_orange
19

Apakah desain fungsional lebih baik daripada desain berorientasi objek? Tergantung.

Apakah MVVM lebih baik daripada MVC? Tergantung.

Amos & Andy atau Martin dan Lewis? Tergantung.

Apa itu tergantung? Pilihan yang Anda buat tergantung pada seberapa baik masing-masing teknik atau teknologi memenuhi persyaratan fungsional dan non-fungsional perangkat lunak Anda, sementara memuaskan tujuan desain, kinerja, dan pemeliharaan Anda.

[Beberapa buku] mengatakan bahwa [sesuatu] salah.

Ketika Anda membaca ini di buku atau blog, evaluasi klaim berdasarkan kemampuannya; yaitu, tanyakan mengapa. Tidak ada teknik benar atau salah dalam pengembangan perangkat lunak, hanya ada "seberapa baik teknik ini memenuhi tujuan saya? Apakah efektif atau tidak efektif? Apakah itu menyelesaikan satu masalah tetapi membuat yang baru? Bisakah itu dipahami dengan baik oleh seluruh tim pengembangan, atau terlalu kabur? "

Dalam kasus khusus ini - tindakan memanggil metode pada objek yang dikembalikan oleh metode lain - karena ada pola desain aktual yang mengkodifikasi praktik ini (Pabrik), sulit untuk membayangkan bagaimana seseorang dapat membuat pernyataan bahwa itu adalah kategori. salah.

Alasannya disebut "Prinsip Pengetahuan Paling Kurang" adalah karena "Kopling Rendah" adalah kualitas sistem yang diinginkan. Objek yang tidak terikat satu sama lain bekerja lebih mandiri, dan karenanya lebih mudah untuk mempertahankan dan memodifikasi secara individual. Tetapi seperti yang ditunjukkan contoh Anda, ada kalanya kopling tinggi lebih diinginkan, sehingga objek dapat lebih efektif mengoordinasikan upaya mereka.

Robert Harvey
sumber
2

Jawaban Doc Brown menunjukkan implementasi buku teks klasik Law of Demeter - dan kekesalan / kekacauan kode-menambahkan beberapa metode yang mungkin mengapa programmer, termasuk saya, sering tidak repot-repot melakukannya, bahkan jika mereka harus.

Ada cara alternatif untuk memisahkan hirarki objek:

Ekspos interfacetipe, bukan classtipe, melalui metode dan properti Anda.

Dalam kasus Original Poster (OP), encoder->WaitEncoderFrame()akan mengembalikan IEncoderFramebukan Frame, dan akan menentukan operasi apa yang diizinkan.


SOLUSI 1

Dalam kasus termudah, Framedan Encoderkelas keduanya berada di bawah kendali Anda, IEncoderFrameadalah bagian dari metode yang sudah diekspos oleh Frame, dan Encoderkelas sebenarnya tidak peduli apa yang Anda lakukan pada objek itu. Kemudian, implementasi bersifat sepele ( kode dalam c # ):

interface IEncoderFrame {
    void DoOrGetSomething();
}

class Frame : IEncoderFrame {
    // A method that already exists in Frame.
    public void DoOrGetSomething() { ... }
}

class Encoder {
    private Frame _frame;
    public IEncoderFrame TheFrame { get { return _frame; } }
    ...
}

SOLUSI 2

Dalam kasus menengah, di mana Framedefinisi tidak berada di bawah kendali Anda, atau tidak sesuai untuk menambahkan IEncoderFramemetode Frame, maka solusi yang baik adalah Adaptor . Itulah yang jawabannya CandiedOrange ini membahas, sebagai new FrameHandler( frame ). PENTING: Jika Anda melakukan ini, itu lebih fleksibel jika Anda mengeksposnya sebagai antarmuka , bukan sebagai kelas . Encoderharus tahu class FrameHandler, tetapi klien hanya perlu tahu interface IFrameHandler. Atau seperti yang saya sebutkan, interface IEncoderFrame- untuk menunjukkan bahwa itu adalah Frame khusus seperti yang terlihat dari POV dari Encoder :

interface IEncoderFrame {
    void DoOrGetSomething();
}

// Adapter pattern. Appropriate if no access needed to Encoder.
class EncoderFrameWrapper : IEncoderFrame {
    Frame _frame;
    public EncoderFrameWrapper( Frame frame ) {
        _frame = frame;
    }
    public void DoOrGetSomething() {
        _frame....;
    }
}

class Encoder {
    private Frame _frame;

    // Adapter pattern. Appropriate if no access needed to Encoder.
    public IEncoderFrame TheFrame { get { return new EncoderFrameWrapper( _frame ); } }

    ...
}

BIAYA: Alokasi dan GC dari objek baru, EncoderFrameWrapper, setiap kali encoder.TheFramedipanggil. (Anda dapat menembolok pembungkus itu, tetapi itu menambahkan lebih banyak kode. Dan hanya mudah untuk kode yang andal jika bidang bingkai pembuat kode tidak dapat diganti dengan bingkai baru.)


SOLUSI 3

Dalam kasus yang lebih sulit, bungkus baru perlu tahu tentang keduanya Encoderdan Frame. Objek itu sendiri akan melanggar LoD - itu memanipulasi hubungan antara Encoder dan Frame yang seharusnya menjadi tanggung jawab Encoder - dan mungkin menjadi rasa sakit untuk mendapatkan yang benar. Inilah yang dapat terjadi jika Anda memulai jalan itu:

interface IEncoderFrame {
    void DoOrGetSomething();
}

// *** You will end up regretting this. See next code snippet instead ***
class EncoderFrameWrapper : IEncoderFrame {
    Encoder _owner;
    Frame _frame;
    public EncoderFrameWrapper( Encoder owner, Frame frame ) {
        _owner = owner;   _frame = frame;
    }
    public void DoOrGetSomething() {
        _frame.DoOrGetSomething();
        // Hmm, maybe this wrapper class should be nested inside Encoder...
        _owner... some work inside owner; maybe should be owner-internal details ...
    }
}

class Encoder {
    private Frame _frame;

    ...
}

Itu jadi jelek. Ada implementasi yang tidak terlalu berbelit-belit, ketika pembungkus perlu menyentuh detail pencipta / pemiliknya (Encoder):

interface IEncoderFrame {
    void DoOrGetSomething();
}

class Encoder : IEncoderFrame {
    private Frame _frame;

    // HA! Client gets to think of this as "the frame object",
    // but its really me, intercepting it.
    public IEncoderFrame TheFrame { get { return this; } }

    // This is the method that the LoD approach suggests writing,
    // except that we are exposing it only when the instance is accessed as an IEncoderFrame,
    // to avoid extending Encoder's already large API surface.
    public void IEncoderFrame.DoOrGetSomething() {
        _frame.DoOrGetSomething();
       ... make some change within current Encoder instance ...
    }
    ...
}

Memang, jika saya tahu saya akan berakhir di sini, saya mungkin tidak akan melakukan ini. Bisa saja menulis metode LoD, dan selesai dengan itu. Tidak perlu mendefinisikan antarmuka. Di sisi lain, saya suka bahwa antarmuka membungkus metode terkait bersama. Saya suka bagaimana rasanya melakukan "operasi seperti bingkai" dengan apa yang terasa seperti bingkai.


KOMENTAR AKHIR

Pertimbangkan ini: Jika implementor Encodermerasa bahwa mengekspos Frame framesesuai dengan arsitektur keseluruhan mereka, atau "jauh lebih mudah daripada mengimplementasikan LoD", maka itu akan jauh lebih aman jika mereka melakukan potongan pertama yang saya tunjukkan - memperlihatkan subset terbatas dari Bingkai, sebagai antarmuka. Dalam pengalaman saya, itu sering merupakan solusi yang sepenuhnya bisa diterapkan. Cukup tambahkan metode ke antarmuka sesuai kebutuhan. (Saya berbicara tentang skenario di mana kita "tahu" Frame sudah memiliki metode yang diperlukan, atau mereka akan mudah dan non-kontroversial untuk ditambahkan. Pekerjaan "implementasi" untuk setiap metode menambahkan satu baris ke definisi antarmuka.) Dan tahu bahwa bahkan dalam skenario terburuk di masa mendatang, dimungkinkan untuk menjaga agar API tetap berfungsi - di sini,IEncoderFrameFrameEncoder.

Juga mencatat bahwa jika Anda tidak memiliki izin untuk menambahkan IEncoderFrameke Frame, atau metode yang diperlukan tidak cocok untuk umum Framekelas, dan solusi # 2 tidak sesuai dengan Anda, mungkin karena objek ekstra penciptaan-dan-kehancuran, solusi # 3 dapat dilihat hanya sebagai cara untuk mengatur metode Encoder, untuk mencapai LoD. Jangan hanya melewati lusinan metode. Bungkus mereka dalam suatu Interface, dan gunakan "implementasi antarmuka eksplisit" (jika Anda berada di c #), sehingga mereka hanya dapat diakses ketika objek dilihat melalui antarmuka itu.

Poin lain yang ingin saya tekankan adalah keputusan untuk mengekspos fungsi sebagai antarmuka , menangani semua 3 situasi yang dijelaskan di atas. Yang pertama, IEncoderFramehanyalah sebagian dari Framefungsionalitas. Yang kedua, IEncoderFrameadalah adaptor. Yang ketiga, IEncoderFrameadalah fungsi partisi ke Encoders. Tidak masalah jika kebutuhan Anda berubah di antara ketiga situasi ini: API tetap sama.

ToolmakerSteve
sumber
Menggabungkan kelas Anda ke antarmuka yang dikembalikan oleh salah satu kolaboratornya, bukan kelas konkret, sementara peningkatan, masih merupakan sumber kopling. Jika antarmuka perlu diubah, itu berarti kelas Anda perlu diubah. Poin penting untuk diambil adalah kopling tidak secara inheren buruk selama Anda memastikan Anda menghindari pasangan yang tidak perlu untuk benda tidak stabil atau struktur internal . Inilah sebabnya mengapa Hukum Demeter perlu diambil dengan sedikit garam; itu memerintahkan Anda untuk selalu menghindari sesuatu yang mungkin atau mungkin tidak menjadi masalah, tergantung pada kondisinya.
Periata Breatta
@PeriataBreatta - Saya tentu saja tidak bisa dan tidak setuju dengan itu. Tapi saya ingin menunjukkan satu hal: Antarmuka , menurut definisi, mewakili apa yang perlu diketahui pada batas antara dua kelas. Jika "perlu diubah", itu mendasar - tidak ada pendekatan alternatif yang bisa secara ajaib menghindari pengkodean yang diperlukan. Bandingkan dengan melakukan salah satu dari tiga situasi yang saya jelaskan, tetapi tidak menggunakan antarmuka- sebagai gantinya, mengembalikan kelas yang konkret. Dalam 1, Bingkai, dalam 2, EncoderFrameWrapper, dalam 3, Encoder. Anda mengunci diri ke yang pendekatan. Antarmuka dapat beradaptasi dengan semuanya.
ToolmakerSteve
@PeriataBreatta ... yang menunjukkan manfaat mendefinisikan secara eksplisit sebagai antarmuka. Saya berharap pada akhirnya untuk meningkatkan IDE untuk membuatnya sangat nyaman, bahwa antarmuka akan digunakan lebih banyak. Sebagian besar akses multi-level akan melalui beberapa antarmuka, karenanya lebih mudah untuk mengelola perubahan. (Jika itu "berlebihan" untuk beberapa kasus, analisis kode, dikombinasikan dengan penjelasan tentang di mana kami bersedia mengambil risiko tidak memiliki antarmuka, dengan imbalan peningkatan kinerja kecil, bisa "kompilasi keluar '- menggantinya dengan salah satu dari 3 kelas konkret dari 3 "solusi".)
ToolmakerSteve
@PeriataBreatta - dan ketika saya mengatakan "antarmuka dapat beradaptasi dengan mereka semua", saya tidak mengatakan "ahhhh, luar biasa bahwa fitur satu bahasa ini dapat mencakup kasus-kasus yang berbeda ini". Saya mengatakan bahwa mendefinisikan antarmuka meminimalkan perubahan yang mungkin perlu Anda lakukan. Dalam kasus terbaik, desain dapat berubah dari case yang paling sederhana (solusi 1), ke case yang paling sulit (solusi 3), tanpa antarmuka yang berubah sama sekali - hanya kebutuhan internal produsen yang menjadi lebih kompleks. Dan bahkan ketika perubahan diperlukan, mereka cenderung kurang meresap, IMHO.
ToolmakerSteve