Rails: Law of Demeter Confusion

13

Saya membaca buku berjudul Rails AntiPatterns dan mereka berbicara tentang menggunakan delegasi untuk menghindari melanggar Hukum Demeter. Inilah contoh utama mereka:

Mereka percaya bahwa memanggil sesuatu seperti ini di controller itu buruk (dan saya setuju)

@street = @invoice.customer.address.street

Solusi yang mereka usulkan adalah melakukan yang berikut:

class Customer

    has_one :address
    belongs_to :invoice

    def street
        address.street
    end
end

class Invoice

    has_one :customer

    def customer_street
        customer.street
    end
end

@street = @invoice.customer_street

Mereka menyatakan bahwa karena Anda hanya menggunakan satu titik, Anda tidak melanggar Hukum Demeter di sini. Saya pikir ini tidak benar, karena Anda masih melalui pelanggan untuk pergi melalui alamat untuk mendapatkan jalan faktur. Saya terutama mendapat ide ini dari posting blog yang saya baca:

http://www.dan-manges.com/blog/37

Dalam posting blog contoh utama adalah

class Wallet
  attr_accessor :cash
end
class Customer
  has_one :wallet

  # attribute delegation
  def cash
    @wallet.cash
  end
end

class Paperboy
  def collect_money(customer, due_amount)
    if customer.cash < due_ammount
      raise InsufficientFundsError
    else
      customer.cash -= due_amount
      @collected_amount += due_amount
    end
  end
end

Posting blog menyatakan bahwa meskipun hanya ada satu titik customer.cashalih-alih customer.wallet.cash, kode ini masih melanggar Hukum Demeter.

Sekarang dalam metode collect_money Paperboy, kami tidak memiliki dua titik, kami hanya memiliki satu di "customer.cash". Apakah delegasi ini menyelesaikan masalah kita? Tidak semuanya. Jika kita melihat perilaku itu, seorang tukang koran masih merogoh dompet pelanggan untuk mendapatkan uang tunai.

EDIT

Saya benar-benar mengerti dan setuju bahwa ini masih merupakan pelanggaran dan saya perlu membuat metode Walletpenarikan yang disebut menangani pembayaran untuk saya dan saya harus memanggil metode itu di dalam Customerkelas. Yang tidak saya dapatkan adalah bahwa menurut proses ini, contoh pertama saya masih melanggar Hukum Demeter karena Invoicemasih menjangkau langsung ke Customerjalan.

Adakah yang bisa membantu saya menghilangkan kebingungan ini? Saya telah mencari selama 2 hari terakhir mencoba untuk membiarkan topik ini masuk, tetapi masih membingungkan.

pengguna2158382
sumber
2
pertanyaan serupa di sini
thorsten müller
Saya tidak berpikir contoh 2 (tukang koran) dari blog melanggar Hukum Demeter. Ini mungkin desain yang buruk (Anda mengasumsikan pelanggan akan membayar dengan uang tunai), tapi itu BUKAN pelanggaran Hukum Demeter. Tidak semua kesalahan desain disebabkan oleh melanggar Hukum ini. Penulis bingung IMO.
Andres F.

Jawaban:

24

Contoh pertama Anda tidak melanggar hukum Demeter. Ya, dengan kode seperti berdiri, mengatakan @invoice.customer_streettidak terjadi untuk mendapatkan yang sama nilai yang hipotetis yang @invoice.customer.address.streetakan, tetapi pada setiap langkah dari traversal, nilai yang dikembalikan ditentukan oleh keberadaan objek bertanya - itu bukan bahwa" mencapai tukang koran ke dalam dompet pelanggan ", itu adalah" tukang koran meminta uang tunai kepada pelanggan, dan pelanggan mendapatkan uang tunai dari dompet mereka ".

Ketika Anda mengatakan @invoice.customer.address.street, Anda mengasumsikan pengetahuan pelanggan dan alamat internal - ini adalah hal yang buruk. Ketika Anda berkata @invoice.customer_street, Anda bertanya invoice, "Hei, saya suka jalan pelanggan, Anda memutuskan bagaimana Anda mendapatkannya ". Pelanggan kemudian berkata ke alamatnya, "Hei, saya suka jalan Anda , Anda memutuskan bagaimana Anda mendapatkannya ".

Dorongan Demeter bukanlah 'Anda tidak akan pernah bisa mengetahui nilai dari objek yang jauh dalam grafik dari Anda "; melainkan' Anda sendiri tidak boleh melintasi jauh sepanjang grafik objek untuk mendapatkan nilai '.

Saya setuju ini mungkin tampak seperti perbedaan yang halus, tetapi pertimbangkan ini: dalam kode Demeter-compliant, berapa banyak kode yang perlu diubah ketika representasi internal suatu addressperubahan? Bagaimana dengan kode yang tidak sesuai dengan Demeter?

AakashM
sumber
Ini persis seperti penjelasan yang saya cari! Terima kasih.
user2158382
Penjelasan yang sangat bagus. Saya punya pertanyaan: 1) Jika objek faktur ingin mengembalikan objek pelanggan ke klien faktur itu tidak berarti bahwa itu adalah objek pelanggan yang sama yang dipegangnya secara internal. Ini mungkin hanya sebuah objek yang dibuat, on the fly, untuk tujuan mengembalikan kepada klien set paket data yang bagus dengan beberapa nilai di dalamnya. Dengan menggunakan logika yang Anda sajikan, Anda mengatakan bahwa faktur tidak dapat memiliki bidang yang mewakili lebih dari satu data. Atau apakah saya melewatkan sesuatu.
zumalifeguard
2

Contoh pertama dan kedua sebenarnya tidak terlalu sama. Sementara yang pertama berbicara tentang aturan umum "satu titik", yang kedua berbicara lebih banyak tentang hal-hal lain dalam desain OO, terutama " Katakan, Jangan tanya "

Delegasi adalah teknik yang efektif untuk menghindari pelanggaran Hukum Demeter, tetapi hanya untuk perilaku, bukan untuk atribut. - Dari contoh kedua, blog Dan

Sekali lagi, " hanya untuk perilaku, bukan untuk atribut "

Jika Anda meminta atribut, Anda seharusnya bertanya . "Hei, teman, berapa banyak uang yang kamu miliki di kantong? Tunjukkan padaku, aku akan mengevaluasi apakah kamu dapat membayar ini." Itu salah, tidak ada petugas belanja akan berperilaku seperti ini. Sebaliknya, mereka akan berkata, "Silakan bayar"

customer.pay(due_amount)

Adalah tugas pelanggan sendiri untuk mengevaluasi apakah ia harus membayar dan apakah ia dapat membayar. Dan tugas panitera selesai setelah memberitahu pelanggan untuk membayar.

Jadi, apakah contoh kedua membuktikan yang pertama salah?

Menurutku. Tidak , asalkan:

1. Anda melakukannya dengan kendala diri.

Meskipun Anda dapat mengakses semua atribut pelanggan @invoicemelalui delegasi, Anda jarang membutuhkannya dalam kasus normal.

Pikirkan tentang halaman yang menampilkan faktur di aplikasi Rails. Akan ada bagian di atas untuk menunjukkan detail pelanggan. Jadi, dalam templat faktur, akankah Anda membuat kode seperti ini?

#customer-info
  = @invoice.customer_name
  = @invoice.customer_address
  ....

Itu salah dan tidak efisien. Pendekatan yang lebih baik adalah

#customer-info
  = render partial: 'invoice_header_customer', 
           locals: {customer: @invoice.customer}

Kemudian biarkan sebagian pelanggan untuk memproses semua atribut milik pelanggan.

Jadi secara umum Anda tidak membutuhkannya. Tapi Anda mungkin memiliki halaman daftar yang menunjukkan semua faktur terbaru, ada bidang pengarahan di setiap limenampilkan nama pelanggan. Dalam hal ini, Anda perlu menunjukkan atribut pelanggan, dan itu benar-benar sah untuk mengkodekan templat sebagai

= @invoice.customer_name

2. Tidak ada tindakan lebih lanjut tergantung pada pemanggilan metode ini.

Dalam kasus laman daftar di atas, faktur menanyakan atribut nama pelanggan, tetapi tujuan sebenarnya adalah " perlihatkan nama Anda ", jadi pada dasarnya masih berupa perilaku tetapi bukan atribut . Tidak ada evaluasi dan tindakan lebih lanjut berdasarkan pada atribut ini seperti, jika nama Anda "Mike", saya akan menyukai Anda dan memberi Anda 30 hari lebih banyak kredit. Tidak, faktur hanya mengatakan "tunjukkan namamu", tidak lebih. Jadi itu benar-benar dapat diterima sesuai dengan aturan "Katakan Jangan Tanya" pada contoh 2.

Billy Chan
sumber
0

Baca lebih lanjut di artikel kedua dan saya pikir idenya akan menjadi lebih jelas. Idenya hanya meminta pelanggan menawarkan kemampuan untuk membayar dan benar-benar menyembunyikan di mana kasing disimpan. Apakah itu bidang, anggota dompet, atau yang lainnya? Penelepon tidak tahu, tidak perlu tahu dan tidak berubah jika detail implementasi berubah.

class Wallet
  attr_accessor :cash
  def withdraw(amount)
     raise InsufficientFundsError if amount > cash
     cash -= amount
     amount
  end
end
class Customer
  has_one :wallet
  # behavior delegation
  def pay(amount)
    @wallet.withdraw(amount)
  end
end
class Paperboy
  def collect_money(customer, due_amount)
    @collected_amount += customer.pay(due_amount)
  end
end

Jadi saya pikir referensi kedua Anda adalah memberikan rekomendasi yang lebih bermanfaat.

Gagasan "satu titik" saja, adalah keberhasilan parsial, karena ia menyembunyikan beberapa detail yang mendalam, tetapi masih dalam tahap peningkatan antara komponen yang terpisah.

djna
sumber
Maaf mungkin saya tidak jelas, tapi saya mengerti contoh kedua dengan sempurna dan saya mengerti bahwa Anda perlu membuat abstraksi yang Anda posting, tetapi apa yang saya tidak mengerti adalah contoh pertama saya. Menurut posting blog, contoh pertama saya salah
user2158382
0

Kedengarannya seperti Dan mencabut contohnya dari artikel ini: The Paperboy, The Wallet, dan The Law Of Demeter

Law Of Demeter Suatu metode suatu objek harus hanya memanggil metode dari jenis objek berikut ini:

  1. diri
  2. parameternya
  3. objek apa pun yang dibuat / instantiate
  4. objek komponen langsungnya

Kapan dan Bagaimana Cara Menerapkan Hukum Demeter

Jadi sekarang Anda memiliki pemahaman yang baik tentang hukum dan manfaatnya, tetapi kami belum membahas cara mengidentifikasi tempat dalam kode yang ada di mana kami dapat menerapkannya (dan sama pentingnya, di mana BUKAN untuk menerapkannya ...)

  1. Pernyataan 'dapatkan' Dirantai - Tempat pertama dan paling jelas untuk menerapkan Law Of Demeter adalah tempat kode yang telah mengulangi get() pernyataan,

    value = object.getX().getY().getTheValue();

    seolah-olah ketika orang kanonik kita untuk contoh ini ditarik oleh polisi, kita mungkin melihat:

    license = person.getWallet().getDriversLicense();

  2. banyak objek 'sementara' - Contoh lisensi di atas tidak akan lebih baik jika kode tersebut terlihat seperti,

    Wallet tempWallet = person.getWallet(); license = tempWallet.getDriversLicense();

    itu setara, tetapi lebih sulit dideteksi.

  3. Mengimpor Banyak Kelas - Pada proyek Java yang saya kerjakan, kami memiliki aturan bahwa kami hanya mengimpor kelas yang benar-benar kami gunakan; Anda tidak pernah melihat sesuatu seperti itu

    import java.awt.*;

    dalam kode sumber kami. Dengan aturan ini berlaku, tidak jarang melihat selusin atau lebih pernyataan impor semuanya berasal dari paket yang sama. Jika ini terjadi dalam kode Anda, ini bisa menjadi tempat yang baik untuk mencari contoh pelanggaran yang tidak jelas. Jika Anda perlu mengimpornya, Anda digabungkan dengannya. Jika itu berubah, Anda mungkin harus melakukannya juga. Dengan mengimpor kelas secara eksplisit, Anda akan mulai melihat bagaimana sebenarnya kelas Anda digabungkan.

Saya mengerti bahwa contoh Anda ada di Ruby, tetapi ini harus berlaku di semua bahasa OOP.

Pak Polywhirl
sumber