Rails: Law of Demeter Confusion


13

Tôi đang đọc một cuốn sách tên là Rails AntiPotypes và họ nói về việc sử dụng ủy quyền để tránh vi phạm Luật Demeter. Đây là ví dụ điển hình của họ:

Họ tin rằng việc gọi một cái gì đó như thế này trong bộ điều khiển là xấu (và tôi đồng ý)

@street = @invoice.customer.address.street

Giải pháp đề xuất của họ là làm như sau:

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

Họ nói rằng vì bạn chỉ sử dụng một dấu chấm, nên bạn không vi phạm Luật Demeter ở đây. Tôi nghĩ rằng điều này là không chính xác, bởi vì bạn vẫn đang thông qua khách hàng để đi qua địa chỉ để lấy đường phố của hóa đơn. Tôi chủ yếu có ý tưởng này từ một bài đăng trên blog mà tôi đọc:

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

Trong bài viết trên blog, ví dụ điển hình là

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

Bài đăng trên blog nói rằng mặc dù chỉ có một dấu chấm customer.cashthay vì customer.wallet.cash, mã này vẫn vi phạm Luật Demeter.

Bây giờ trong phương thức Paperboy coll_money, chúng tôi không có hai dấu chấm, chúng tôi chỉ có một dấu chấm trong "customer.cash". Đoàn này đã giải quyết vấn đề của chúng tôi? Không có gì. Nếu chúng ta xem xét hành vi, một người bán báo vẫn tiếp cận trực tiếp vào ví của khách hàng để rút tiền.

BIÊN TẬP

Tôi hoàn toàn hiểu và đồng ý rằng đây vẫn là một vi phạm và tôi cần tạo một phương thức Walletgọi là rút tiền xử lý khoản thanh toán cho tôi và tôi nên gọi phương thức đó trong Customerlớp. Những gì tôi không nhận được là theo quy trình này, ví dụ đầu tiên của tôi vẫn vi phạm Luật Demeter vì Invoicevẫn tiếp cận trực tiếp Customerđể đi ra đường.

Ai đó có thể giúp tôi xóa sự nhầm lẫn. Tôi đã tìm kiếm trong 2 ngày qua để cố gắng để chủ đề này chìm vào, nhưng nó vẫn còn khó hiểu.


2
câu hỏi tương tự ở đây
thorsten müller

Tôi không nghĩ ví dụ thứ 2 (người bán báo) từ blog vi phạm Luật Demeter. Nó có thể là thiết kế tồi (bạn cho rằng khách hàng sẽ trả bằng tiền mặt), nhưng đó KHÔNG phải là vi phạm Luật của Demeter. Không phải tất cả các lỗi thiết kế là do vi phạm Luật này. Tác giả đang nhầm lẫn IMO.
Andres F.

Câu trả lời:


24

Ví dụ đầu tiên của bạn không vi phạm luật của Demeter. Đúng, với mã như hiện tại, việc nói @invoice.customer_streetsẽ xảy ra để có cùng giá trị như giả thuyết @invoice.customer.address.street, nhưng ở mỗi bước của giao dịch, giá trị được trả lại được quyết định bởi đối tượng được hỏi - không phải là "người bán báo đạt được ví của khách hàng ", đó là" người bán báo yêu cầu khách hàng lấy tiền mặt và khách hàng tình cờ lấy tiền từ ví của họ ".

Khi bạn nói @invoice.customer.address.street, bạn đang thừa nhận kiến ​​thức về khách hàng và địa chỉ nội bộ - đây là điều xấu. Khi bạn nói @invoice.customer_street, bạn đang hỏi invoice, "này, tôi thích đường phố của khách hàng, bạn quyết định cách bạn có được nó ". Sau đó, khách hàng nói với địa chỉ của nó, "hey tôi thích đường phố của bạn, bạn quyết định cách bạn có được nó ".

Lực đẩy của Demeter không phải là "bạn không bao giờ có thể biết các giá trị từ các đối tượng ở xa trong biểu đồ", thay vào đó, " bản thân bạn không được đi xa dọc theo biểu đồ đối tượng để có được các giá trị".

Tôi đồng ý điều này có vẻ như là một sự khác biệt tinh tế, nhưng hãy xem xét điều này: trong mã tuân thủ Demeter, cần bao nhiêu mã để thay đổi khi biểu diễn bên trong của một addressthay đổi? Điều gì về mã không tuân thủ Demeter?


Đây chính xác là loại giải thích tôi đang tìm kiếm! Cảm ơn bạn.
dùng2158382

Giải thích rất tốt. Tôi có một câu hỏi: 1) Nếu đối tượng hóa đơn muốn trả lại một đối tượng khách hàng cho khách hàng của hóa đơn không nhất thiết có nghĩa đó là cùng một đối tượng khách hàng mà nó giữ bên trong. Nó có thể chỉ đơn giản là một đối tượng được tạo ra, một cách nhanh chóng, với mục đích trả lại cho khách hàng một bộ dữ liệu được đóng gói đẹp với nhiều giá trị trong đó. Sử dụng logic bạn trình bày, bạn đang nói rằng hóa đơn không thể có một trường đại diện cho nhiều dữ liệu. Hay tôi đang thiếu một cái gì đó.
bảo vệ zumalififard

2

Ví dụ đầu tiên và thứ hai thực sự không giống nhau. Trong khi phần đầu tiên nói về các quy tắc chung của "một dấu chấm", thì phần thứ hai nói nhiều hơn về những điều khác trong thiết kế OO, đặc biệt là " Nói, Đừng hỏi "

Phân quyền là một kỹ thuật hiệu quả để tránh vi phạm Luật của Demeter, nhưng chỉ cho hành vi, không phải cho các thuộc tính. - Từ ví dụ thứ hai, blog của Dan

Một lần nữa, " chỉ cho hành vi, không phải cho các thuộc tính "

Nếu bạn yêu cầu thuộc tính, bạn có nghĩa vụ phải hỏi . "Này, anh bạn, bạn có bao nhiêu tiền trong túi? Hãy cho tôi xem, tôi sẽ đánh giá xem bạn có thể trả khoản này không." Điều đó là sai, không có nhân viên mua sắm sẽ hành xử như thế này. Thay vào đó, họ sẽ nói, "Xin hãy trả tiền"

customer.pay(due_amount)

Trách nhiệm của riêng khách hàng là đánh giá xem anh ta có nên trả tiền không và anh ta có thể trả không. Và nhiệm vụ của nhân viên bán hàng đã kết thúc sau khi bảo khách hàng trả tiền.

Vì vậy, ví dụ thứ hai có chứng minh điều thứ nhất là sai không?

Theo ý kiến ​​của tôi. Không , miễn là:

1. Bạn làm điều đó với sự tự ràng buộc.

Mặc dù bạn có thể truy cập tất cả các thuộc tính của khách hàng @invoicetheo ủy quyền, nhưng bạn hiếm khi cần điều đó trong các trường hợp thông thường.

Hãy suy nghĩ về một trang hiển thị hóa đơn trong ứng dụng Rails. Sẽ có một phần trên đầu để hiển thị chi tiết của khách hàng. Vì vậy, trong mẫu hóa đơn, bạn sẽ mã như thế này?

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

Điều đó sai và không hiệu quả. Một cách tiếp cận tốt hơn là

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

Sau đó, để cho khách hàng một phần để xử lý tất cả các thuộc tính thuộc về khách hàng.

Vì vậy, nói chung bạn không cần điều đó. Nhưng bạn có thể có một trang danh sách hiển thị tất cả các hóa đơn gần đây, có một trường tóm tắt trong mỗi lihiển thị tên của khách hàng. Trong trường hợp này, bạn cần phải hiển thị thuộc tính của khách hàng và việc mã hóa mẫu là hoàn toàn hợp pháp

= @invoice.customer_name

2. Không có thêm hành động tùy thuộc vào cuộc gọi phương thức này.

Trong trường hợp trên của trang danh sách, hóa đơn đã hỏi thuộc tính tên của khách hàng, nhưng mục đích thực sự của nó là " cho tôi biết tên của bạn ", vì vậy về cơ bản nó vẫn là một hành vi nhưng không phải là thuộc tính . Không có đánh giá và hành động nào nữa dựa trên thuộc tính này, nếu tên của bạn là "Mike" tôi sẽ thích bạn và cho bạn thêm 30 ngày tín dụng. Không, hóa đơn chỉ cần nói "cho tôi biết tên của bạn", không còn nữa. Vì vậy, điều đó hoàn toàn chấp nhận được theo quy tắc "Nói không hỏi" trong ví dụ 2.


0

Đọc thêm trong bài viết thứ hai và tôi nghĩ ý tưởng sẽ trở nên rõ ràng hơn. Ý tưởng chỉ cần khách hàng cung cấp một khả năng để thanh toán và hoàn toàn che giấu nơi vụ án được lưu giữ. Nó là một lĩnh vực, một thành viên của ví, hoặc cái gì khác? Người gọi không biết, không cần biết và không thay đổi nếu chi tiết triển khai đó thay đổi.

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

Vì vậy, tôi nghĩ rằng tài liệu tham khảo thứ hai của bạn đang đưa ra một khuyến nghị hữu ích hơn.

Ý tưởng duy nhất "một dấu chấm", là một thành công một phần, trong đó nó ẩn giấu một số chi tiết sâu sắc, nhưng vẫn bao gồm các khớp nối giữa các thành phần riêng biệt.


Xin lỗi có thể tôi không rõ ràng, nhưng tôi hiểu ví dụ thứ hai một cách hoàn hảo và tôi hiểu rằng bạn cần thực hiện sự trừu tượng mà bạn đã đăng, nhưng những gì tôi không hiểu là ví dụ đầu tiên của tôi. Theo bài đăng trên blog, ví dụ đầu tiên của tôi không chính xác
user2158382

0

Âm thanh như Dan lấy ví dụ của anh ấy từ bài viết này: The Paperboy, The Wallet, và The Law Of Demeter

Law Of Demeter Một phương thức của một đối tượng chỉ nên gọi các phương thức của các loại đối tượng sau:

  1. chinh no
  2. thông số của nó
  3. bất kỳ đối tượng nào nó tạo / khởi tạo
  4. đối tượng thành phần trực tiếp của nó

Khi nào và làm thế nào để áp dụng Luật của Demeter

Vì vậy, bây giờ bạn đã hiểu rõ về luật pháp và lợi ích của nó, nhưng chúng tôi chưa thảo luận về cách xác định các vị trí trong mã hiện tại nơi chúng tôi có thể áp dụng nó (và cũng quan trọng, nơi KHÔNG áp dụng nó ...)

  1. Các tuyên bố 'nhận' được xâu chuỗi - Nơi đầu tiên, rõ ràng nhất để áp dụng Law Of Demeter là những nơi có mã được lặp đi lặp lại get() ,

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

    như thể khi người kinh điển của chúng ta cho ví dụ này bị cảnh sát kéo qua, chúng ta có thể thấy:

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

  2. nhiều đối tượng 'tạm thời' - Ví dụ về giấy phép ở trên sẽ không tốt hơn nếu mã trông như thế nào,

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

    nó là tương đương, nhưng khó phát hiện hơn

  3. Nhập nhiều lớp - Trong dự án Java mà tôi làm việc, chúng tôi có một quy tắc là chúng tôi chỉ nhập các lớp mà chúng tôi thực sự sử dụng; bạn không bao giờ thấy một cái gì đó như

    import java.awt.*;

    trong mã nguồn của chúng tôi. Với quy tắc này được áp dụng, sẽ không có gì lạ khi thấy hàng tá báo cáo nhập khẩu đều xuất phát từ cùng một gói. Nếu điều này xảy ra trong mã của bạn, nó có thể là một nơi tốt để tìm kiếm các ví dụ vi phạm bị che khuất. Nếu bạn cần nhập nó, bạn được ghép nối với nó. Nếu nó thay đổi, bạn có thể phải như vậy. Bằng cách nhập rõ ràng các lớp, bạn sẽ bắt đầu thấy các lớp của bạn thực sự được ghép như thế nào.

Tôi hiểu rằng ví dụ của bạn là trong Ruby, nhưng điều này sẽ áp dụng trên tất cả các ngôn ngữ OOP.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.