Rails: กฎแห่งความสับสนของ Demeter


13

ฉันกำลังอ่านหนังสือชื่อ Rails AntiPatterns และพวกเขาคุยกันเรื่องการใช้การมอบหมายเพื่อหลีกเลี่ยงการทำผิดกฎของ Demeter นี่คือตัวอย่างสำคัญของพวกเขา:

พวกเขาเชื่อว่าการโทรแบบนี้ในคอนโทรลเลอร์ไม่ดี (และฉันเห็นด้วย)

@street = @invoice.customer.address.street

ทางออกที่เสนอของพวกเขาคือทำสิ่งต่อไปนี้:

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

พวกเขากำลังระบุว่าเนื่องจากคุณใช้เพียงจุดเดียวคุณจะไม่ละเมิดกฎของ Demeter ที่นี่ ฉันคิดว่านี่ไม่ถูกต้องเพราะคุณยังคงต้องผ่านลูกค้าเพื่อผ่านที่อยู่เพื่อรับถนนใบแจ้งหนี้ ฉันได้รับแนวคิดนี้จากโพสต์บล็อกที่ฉันอ่านเป็นหลัก:

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

ในบล็อกโพสต์ตัวอย่างสำคัญคือ

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

บล็อกโพสต์ระบุว่าแม้จะมีจุดเดียวcustomer.cashแทนcustomer.wallet.cashรหัสนี้ยังคงละเมิดกฎหมายของ Demeter

ตอนนี้ในวิธี Paperboy collect_money เราไม่มีจุดสองจุดเรามีเพียงจุดเดียวใน "customer.cash" การมอบหมายนี้แก้ไขปัญหาของเราได้หรือไม่? ไม่ใช่เลย. หากเราดูที่พฤติกรรมเด็กชายกระดาษยังคงเข้าถึงกระเป๋าเงินของลูกค้าโดยตรงเพื่อรับเงินสด

แก้ไข

ฉันเข้าใจและยอมรับอย่างสมบูรณ์ว่านี่เป็นการละเมิดและฉันจำเป็นต้องสร้างวิธีการในการWalletถอนเงินที่เรียกว่าจัดการการจ่ายเงินให้ฉันและฉันควรเรียกวิธีการนั้นในCustomerชั้นเรียน สิ่งที่ฉันไม่ได้รับก็คือตามกระบวนการนี้ตัวอย่างแรกของฉันยังคงละเมิดกฎหมายของ Demeter เพราะInvoiceยังเข้าถึงโดยตรงCustomerเพื่อไปตามถนน

ใครบางคนสามารถช่วยฉันล้างความสับสน ฉันค้นหามาตลอด 2 วันที่ผ่านมาพยายามปล่อยให้หัวข้อนี้จม แต่มันก็ยังสับสนอยู่


2
คำถามที่คล้ายกันที่นี่
thorsten müller

ฉันไม่คิดว่าตัวอย่างที่ 2 (หนังสือพิมพ์) จากบล็อกละเมิดกฎหมายของ Demeter อาจเป็นการออกแบบที่ไม่ดี (คุณสมมติว่าลูกค้าชำระด้วยเงินสด) แต่นั่นไม่ใช่การละเมิดกฎหมายของ Demeter ข้อผิดพลาดในการออกแบบบางอย่างอาจเกิดจากการละเมิดกฎหมาย ผู้เขียนสับสน IMO
Andres F.

1
กรุณาอย่าโพสต์คำถามเดียวกันบนเว็บไซต์ต่างๆ
Gilles 'หยุดความชั่วร้าย'

คำตอบ:


24

ตัวอย่างแรกของคุณไม่ได้ละเมิดกฎหมายของ Demeter ใช่ด้วยรหัสตามที่กล่าวว่า@invoice.customer_streetเกิดขึ้นเพื่อให้ได้ค่าเดียวกับที่สมมุติ@invoice.customer.address.streetแต่ในแต่ละขั้นตอนของการสำรวจเส้นทางค่าที่ส่งคืนจะถูกตัดสินโดยวัตถุที่ถูกถาม - ไม่ใช่ว่า "เด็กกระดาษถึง กระเป๋าเงินของลูกค้า "มันคือ" เด็กชายกระดาษขอเงินสดและลูกค้าได้รับเงินสดจากกระเป๋าของพวกเขา "

เมื่อคุณพูด@invoice.customer.address.streetคุณกำลังสมมติความรู้เกี่ยวกับลูกค้าและที่อยู่ภายใน - นี่คือสิ่งที่ไม่ดี เมื่อคุณพูดว่า@invoice.customer_streetคุณกำลังถามinvoice"เฮ้ฉันต้องการถนนของลูกค้าคุณตัดสินใจว่าคุณจะได้รับมัน " จากนั้นลูกค้าพูดกับที่อยู่ของลูกค้าว่า "เฮ้ฉันต้องการถนนของคุณคุณเป็นผู้ตัดสินใจว่าจะรับมันอย่างไร "

การแทง Demeter ไม่ได้เป็น'คุณไม่เคยรู้คุณค่าจากวัตถุที่อยู่ไกลในกราฟจากคุณ "แต่มันคือ' คุณเองไม่ต้องเดินทางไกลไปตามกราฟวัตถุเพื่อรับค่า '

ฉันยอมรับว่าสิ่งนี้อาจดูเหมือนความแตกต่างที่ละเอียดอ่อน แต่พิจารณาในรหัสที่ Demeter-compliant จำเป็นต้องเปลี่ยนรหัสจำนวนเท่าใดเมื่อการแสดงภายในของการaddressเปลี่ยนแปลงมีผล? แล้วโค้ดที่ไม่เข้ากับ Demeter ล่ะ?


นี่เป็นคำอธิบายที่ฉันกำลังมองหา! ขอขอบคุณ.
user2158382

คำอธิบายที่ดีมาก ฉันมีคำถาม: 1) ถ้าวัตถุใบแจ้งหนี้ต้องการส่งคืนวัตถุของลูกค้าไปยังลูกค้าของใบแจ้งหนี้ที่ไม่จำเป็นต้องหมายความว่าเป็นวัตถุลูกค้าเดียวกันกับที่ถือภายใน มันอาจเป็นเพียงแค่วัตถุที่สร้างขึ้นโดยทันทีเพื่อวัตถุประสงค์ในการส่งคืนชุดข้อมูลที่ดีซึ่งบรรจุด้วยค่าหลายค่า การใช้ตรรกะที่คุณนำเสนอคุณกำลังบอกว่าใบแจ้งหนี้ไม่สามารถมีเขตข้อมูลที่แสดงข้อมูลมากกว่าหนึ่งรายการ หรือฉันกำลังคิดถึงบางสิ่งอยู่
zumalifeguard

2

ตัวอย่างแรกและครั้งที่สองนั้นไม่เหมือนกันจริงๆ ในขณะที่การพูดคุยครั้งแรกเกี่ยวกับกฎทั่วไปของ "หนึ่งจุด" ที่สองพูดถึงสิ่งอื่น ๆ เพิ่มเติมในการออกแบบ OO โดยเฉพาะ " บอกอย่าถาม "

การมอบหมายเป็นเทคนิคที่มีประสิทธิภาพเพื่อหลีกเลี่ยงการละเมิดกฎของ Demeter แต่สำหรับพฤติกรรมเท่านั้นไม่ใช่สำหรับคุณลักษณะ - จากตัวอย่างที่สองบล็อกของแดน

อีกครั้ง " สำหรับพฤติกรรมเท่านั้นไม่ใช่สำหรับแอตทริบิวต์ "

หากคุณถามสำหรับแอตทริบิวต์คุณควรจะถาม "เฮ้ผู้ชายเงินในกระเป๋าเท่าไหร่แสดงให้ฉันดูฉันจะประเมินว่าคุณสามารถจ่ายได้หรือไม่" มันผิดปกติพนักงานช็อปปิ้งจะไม่ทำตัวแบบนี้ พวกเขาจะพูดว่า "โปรดจ่าย"

customer.pay(due_amount)

มันจะเป็นหน้าที่ของลูกค้าในการประเมินว่าเขาควรจะจ่ายและถ้าเขาสามารถจ่ายได้ และงานของพนักงานเสร็จสิ้นหลังจากแจ้งให้ลูกค้าชำระเงิน

ดังนั้นตัวอย่างที่สองพิสูจน์ว่าข้อแรกผิดหรือเปล่า?

ในความเห็นของฉัน. ไม่ได้ตราบใดที่:

1. คุณทำได้ด้วยการ จำกัด ตัวเอง

ในขณะที่คุณสามารถเข้าถึงคุณลักษณะทั้งหมดของลูกค้าได้@invoiceโดยการมอบหมายคุณไม่ค่อยต้องการในกรณีปกติ

คิดถึงหน้าแสดงใบแจ้งหนี้ในแอพ Rails จะมีส่วนด้านบนเพื่อแสดงรายละเอียดของลูกค้า ดังนั้นในเทมเพลตใบแจ้งหนี้คุณจะรหัสเช่นนี้หรือไม่?

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

มันผิดและไร้ประสิทธิภาพ แนวทางที่ดีกว่าคือ

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

จากนั้นปล่อยให้ลูกค้าบางส่วนประมวลผลคุณลักษณะทั้งหมดที่เป็นของลูกค้า

โดยทั่วไปแล้วคุณไม่ต้องการสิ่งนั้น แต่คุณอาจมีหน้ารายการที่แสดงใบแจ้งหนี้ล่าสุดทั้งหมดมีฟิลด์สรุปข้อมูลในแต่ละรายการที่liแสดงชื่อลูกค้า ในกรณีนี้คุณต้องแสดงแอตทริบิวต์ของลูกค้าและเป็นเรื่องที่ถูกต้องโดยสิ้นเชิงที่จะเขียนโค้ดเทมเพลตเป็น

= @invoice.customer_name

2. ไม่มีการดำเนินการเพิ่มเติมขึ้นอยู่กับการเรียกใช้วิธีนี้

ในกรณีข้างต้นของหน้ารายการใบแจ้งหนี้ที่ถามแอตทริบิวต์ชื่อลูกค้า แต่มันก็เป็นจุดประสงค์ที่แท้จริงคือ " แสดงให้ฉันชื่อของคุณ " ดังนั้นจึงเป็นพื้นยังคงเป็นลักษณะการทำงานแต่ไม่แอตทริบิวต์ ไม่มีการประเมินและการดำเนินการเพิ่มเติมตามคุณลักษณะนี้เช่นหากชื่อของคุณคือ "ไมค์" ฉันจะชอบคุณและให้เครดิตแก่คุณอีก 30 วัน ไม่ใบแจ้งหนี้เพียงแค่พูดว่า "แสดงชื่อของคุณ" ไม่อีกแล้ว ดังนั้นมันจึงเป็นที่ยอมรับโดยสิ้นเชิงตามกฎ "อย่าบอกเลย" ในตัวอย่างที่ 2


0

อ่านเพิ่มเติมในบทความที่สองและฉันคิดว่าความคิดจะชัดเจนขึ้น แนวคิดมีเพียงลูกค้าที่เสนอความสามารถในการชำระเงินและซ่อนอย่างสมบูรณ์ในกรณีที่ถูกเก็บไว้ มันเป็นเขตข้อมูลสมาชิกของกระเป๋าเงินหรืออย่างอื่นหรือไม่? ผู้โทรไม่ทราบไม่จำเป็นต้องรู้และไม่เปลี่ยนแปลงหากรายละเอียดการใช้งานเปลี่ยนแปลงไป

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

ดังนั้นฉันคิดว่าการอ้างอิงที่สองของคุณคือการให้คำแนะนำที่เป็นประโยชน์มากขึ้น

แนวคิด "หนึ่งจุด" เป็นความสำเร็จเพียงบางส่วนซึ่งมันซ่อนรายละเอียดบางส่วนไว้ แต่ยังคงมีการเชื่อมต่อระหว่างส่วนประกอบต่างๆ


ขออภัยบางทีฉันยังไม่ชัดเจน แต่ฉันเข้าใจตัวอย่างที่สองอย่างสมบูรณ์และฉันเข้าใจว่าคุณต้องทำให้สิ่งที่คุณโพสต์เป็นนามธรรม แต่สิ่งที่ฉันไม่เข้าใจเป็นตัวอย่างแรกของฉัน ตามโพสต์บล็อกตัวอย่างแรกของฉันไม่ถูกต้อง
user2158382

0

ฟังดูเหมือนแดนจะยกตัวอย่างของเขาจากบทความนี้: The Paperboy, The Wallet และ The Law Of Demeter

กฎหมายของ Demeter วิธีการของวัตถุควรเรียกใช้วิธีการของวัตถุชนิดต่อไปนี้เท่านั้น:

  1. ตัวเอง
  2. พารามิเตอร์ของมัน
  3. วัตถุใด ๆ ที่มันสร้าง / instantiates
  4. วัตถุองค์ประกอบโดยตรง

เวลาและวิธีการใช้กฎหมายของ Demeter

ตอนนี้คุณมีความเข้าใจกฎหมายและประโยชน์เป็นอย่างดี แต่เรายังไม่ได้พูดถึงวิธีการระบุสถานที่ในรหัสที่มีอยู่ซึ่งเราสามารถนำไปใช้ได้ (และที่สำคัญเช่นเดียวกับที่ไม่ควรใช้ ... )

  1. ข้อความ 'รับ' ถูกล่ามโซ่ - สถานที่แรกที่ชัดเจนที่สุดในการใช้กฎหมายของ Demeter คือสถานที่ของรหัสที่มีget() ข้อความซ้ำ

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

    ราวกับว่าเมื่อบุคคลที่เป็นที่ยอมรับของเราสำหรับตัวอย่างนี้ถูกตำรวจเข้ามาเราอาจเห็น:

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

  2. วัตถุ 'ชั่วคราว' มากมาย - ตัวอย่างใบอนุญาตด้านบนจะไม่ดีกว่าถ้ารหัสดูเหมือน

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

    มันเทียบเท่า แต่ยากที่จะตรวจจับ

  3. การนำเข้าหลายคลาส - ในโครงการ Java ที่ฉันทำงานเรามีกฎที่เรานำเข้าเฉพาะคลาสที่เราใช้จริงเท่านั้น คุณไม่เคยเห็นอะไรแบบนี้

    import java.awt.*;

    ในซอร์สโค้ดของเรา ด้วยกฎนี้เข้าด้วยกันจึงไม่ใช่เรื่องแปลกที่จะเห็นคำสั่งการนำเข้าหลายสิบคำสั่งทั้งหมดที่มาจากแพ็คเกจเดียวกัน หากสิ่งนี้เกิดขึ้นในรหัสของคุณอาจเป็นที่ที่ดีในการมองหาตัวอย่างของการละเมิดที่ไม่ชัดเจน หากคุณต้องการนำเข้าคุณเป็นคู่กับมัน หากมีการเปลี่ยนแปลงคุณอาจต้องทำเช่นกัน โดยการนำเข้าชั้นเรียนอย่างชัดเจนคุณจะเริ่มเห็นว่าคู่ของคุณเป็นอย่างไร

ฉันเข้าใจว่าตัวอย่างของคุณเป็น Ruby แต่ควรใช้กับทุกภาษา OOP

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.