Szyny: Prawo zamieszania Demeter

13

Czytam książkę zatytułowaną Rails AntiPatterns, a oni mówią o korzystaniu z delegacji, aby uniknąć łamania Prawa Demeter. Oto ich najlepszy przykład:

Uważają, że wywołanie czegoś takiego w kontrolerze jest złe (i zgadzam się)

@street = @invoice.customer.address.street

Ich proponowanym rozwiązaniem jest wykonanie następujących czynności:

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

Mówią, że skoro używasz tylko jednej kropki, nie łamiesz tutaj Prawa Demetera. Myślę, że jest to niepoprawne, ponieważ nadal przechodzisz przez klienta, aby przejść przez adres, aby uzyskać ulicę z fakturą. Ten pomysł pochodzi przede wszystkim z postu na blogu, który przeczytałem:

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

W poście na blogu najlepszym przykładem jest

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

Stany wpis na blogu, że chociaż istnieje tylko jedna kropka customer.cashzamiast customer.wallet.cashtego kodu nadal narusza Prawo Demeter.

Teraz w metodzie Paperboy Collect_money nie mamy dwóch kropek, mamy tylko jedną w „customer.cash”. Czy ta delegacja rozwiązała nasz problem? Ani trochę. Jeśli spojrzymy na to zachowanie, gazeciarz wciąż sięga bezpośrednio do portfela klienta, aby uzyskać gotówkę.

EDYTOWAĆ

W pełni rozumiem i zgadzam się, że to nadal stanowi naruszenie i muszę stworzyć metodę Walletzwaną wypłatą, która obsłuży dla mnie płatność i że powinienem wywołać tę metodę w Customerklasie. Nie rozumiem, że zgodnie z tym procesem mój pierwszy przykład wciąż narusza Prawo Demetera, ponieważ Invoicewciąż sięga bezpośrednio Customerpo ulicę.

Czy ktoś może mi pomóc usunąć zamieszanie. Przez ostatnie 2 dni szukałem sposobu, aby ten temat zagłębił się, ale nadal jest mylący.

użytkownik2158382
źródło
2
podobne pytanie tutaj
thorsten müller
Nie sądzę, aby drugi przykład (gazeciarz) z bloga naruszał Prawo Demetera. Może to być zły projekt (zakładasz, że klient zapłaci gotówką), ale to NIE jest naruszenie prawa Demetera. Nie wszystkie błędy projektowe są spowodowane złamaniem tego Prawa. Autor jest zdezorientowany IMO.
Andres F.,
1
Nie publikuj tego samego pytania na wielu stronach .
Gilles 'SO - przestań być zły'

Odpowiedzi:

24

Twój pierwszy przykład nie narusza prawa Demeter. Tak, przy obecnym kodzie mówiąc @invoice.customer_street, zdarza się, że dostaje taką samą wartość , jak hipotetyczna @invoice.customer.address.street, ale na każdym etapie przejścia o wartości zwracanej decyduje pytany obiekt - nie chodzi o to, że „gazeciarz sięga do portfel klienta ”, to znaczy, że„ gazeciarz prosi klienta o gotówkę, a klient otrzymuje gotówkę z portfela ”.

Kiedy mówisz @invoice.customer.address.street, jesteś przy założeniu znajomości klienta i adres wewnętrznych - to jest złe. Kiedy mówisz @invoice.customer_street, pytasz: invoice„hej, chciałbym, żeby ulica klienta, ty decydujesz, jak ją zdobyć ”. Następnie klient mówi na swój adres: „hej, chciałbym, żeby twoja ulica decydowała, jak ją zdobyć ”.

Nacisk Demetera nie polega na tym , że „nigdy nie możesz poznać wartości z obiektów znajdujących się daleko na wykresie od ciebie”, lecz „ samemu nie wolno przechodzić daleko wzdłuż wykresu obiektowego w celu uzyskania wartości”.

Zgadzam się, że może to wydawać się subtelnym rozróżnieniem, ale rozważ to: w kodzie zgodnym z Demeter, ile kodu musi się zmienić, gdy wewnętrzna reprezentacja addresszmian? Co z kodem niezgodnym z Demeter?

AakashM
źródło
Właśnie takiego wyjaśnienia szukałem! Dziękuję Ci.
user2158382 17.10.13
Bardzo dobre wytłumaczenie. Mam pytania: 1) Jeśli obiekt faktury chce zwrócić obiekt klienta klientowi faktury, niekoniecznie oznacza to, że jest to ten sam obiekt klienta, który posiada wewnętrznie. Może to być po prostu obiekt tworzony w locie w celu zwrócenia klientowi ładnego, spakowanego zestawu danych z wieloma wartościami. Korzystając z przedstawionej logiki, mówisz, że faktura nie może mieć pola reprezentującego więcej niż jedno dane. A może coś mi brakuje.
zumalifeguard
2

Pierwszy przykład i drugi nie są właściwie takie same. Podczas gdy pierwsza mówi o ogólnych zasadach „jednej kropki”, druga mówi więcej o innych rzeczach w projektowaniu OO, szczególnie „ Mów, nie pytaj

Delegowanie to skuteczna technika pozwalająca uniknąć naruszeń prawa Demetera, ale tylko w przypadku zachowania, a nie atrybutów. - Z drugiego przykładu blog Dana

Znowu „ tylko dla zachowania, a nie dla atrybutów

Jeśli poprosisz o atrybuty, powinieneś zapytać . „Hej, koleś, ile masz pieniędzy w kieszeni? Pokaż, ocenię, czy możesz to zapłacić”. To nie tak, żaden sprzedawca nie będzie się tak zachowywał. Zamiast tego powiedzą: „Proszę zapłacić”

customer.pay(due_amount)

Obowiązkiem klienta będzie ocena, czy powinien zapłacić i czy może zapłacić. Zadanie urzędnika kończy się po wezwaniu klienta do zapłaty.

Czy zatem drugi przykład dowodzi, że pierwszy jest błędny?

W mojej opinii. Nie , o ile:

1. Robisz to z samoograniczeniem.

Chociaż możesz uzyskać dostęp do wszystkich atrybutów klienta w @invoicedrodze delegacji, rzadko jest to potrzebne w normalnych przypadkach.

Pomyśl o stronie przedstawiającej fakturę w aplikacji Rails. Na górze będzie sekcja pokazująca dane klienta. Czy w szablonie faktury kodujesz w ten sposób?

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

To źle i nieefektywnie. Lepszym podejściem jest

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

Następnie pozwól klientowi częściowo przetworzyć wszystkie atrybuty należące do klienta.

Więc generalnie nie potrzebujesz tego. Ale możesz mieć stronę z listą wszystkich ostatnich faktur, w każdej znajduje się pole informacyjne liz nazwą klienta. W takim przypadku potrzebujesz atrybutu klienta, aby wyświetlić, i całkowicie kodować szablon jako

= @invoice.customer_name

2. W zależności od tego wywołania metody nie ma dalszych działań.

W powyższym przypadku strony z listą faktura wymagała atrybutu nazwy klienta, ale jej prawdziwym celem jest „ pokaż mi swoje imię ”, więc zasadniczo jest to zachowanie, ale nie atrybut . Nie ma dalszej oceny i działań opartych na tym atrybucie, np. Jeśli masz na imię „Mike”, polubię cię i dam ci 30 dni więcej uznania. Nie, faktura mówi tylko „pokaż mi swoje imię”, nigdy więcej. Jest to więc całkowicie dopuszczalne zgodnie z regułą „Tell Don't Ask” w przykładzie 2.

Billy Chan
źródło
0

Czytaj dalej w drugim artykule i myślę, że pomysł stanie się jaśniejszy. Pomysł polega na zaoferowaniu klientowi możliwości zapłaty i całkowitego ukrycia miejsca przechowywania. Czy to pole, członek portfela czy coś innego? Dzwoniący nie wie, nie musi wiedzieć i nie zmienia się, jeśli zmieniają się szczegóły implementacji.

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

Myślę więc, że twoje drugie odniesienie daje bardziej pomocne rekomendacje.

Pomysł „jedna kropka” jest częściowym sukcesem, ponieważ kryje w sobie głęboki szczegół, ale wciąż zwiększa sprzężenie między oddzielnymi komponentami.

djna
źródło
Przepraszam, może nie byłem jasny, ale doskonale rozumiem drugi przykład i rozumiem, że musisz dokonać zamieszczonej przez ciebie abstrakcji, ale nie rozumiem tego jako mojego pierwszego przykładu. Zgodnie z postem na blogu mój pierwszy przykład jest niepoprawny
user2158382
0

Wygląda na to, że Dan wyciągnął swój przykład z tego artykułu: Paperboy, The Wallet i The Law Of Demeter

Law Of Demeter Metoda obiektu powinna wywoływać tylko metody następujących rodzajów obiektów:

  1. samo
  2. jego parametry
  3. wszelkie obiekty, które tworzy / tworzy
  4. jego bezpośrednie obiekty składowe

Kiedy i jak stosować prawo Demeter

Więc teraz dobrze rozumiesz prawo i jego zalety, ale nie dyskutowaliśmy jeszcze, jak zidentyfikować miejsca w istniejącym kodzie, w których możemy go zastosować (i równie ważne, gdzie NIE należy go stosować ...)

  1. Łańcuchowe instrukcje „get” - pierwszym, najbardziej oczywistym miejscem zastosowania prawa Demetera są miejsca kodu, które mają powtarzające się get() instrukcje,

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

    tak jakby policjant przyciągnął naszą kanoniczną osobę na ten przykład, możemy zobaczyć:

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

  2. wiele „tymczasowych” obiektów - powyższy przykład licencji nie byłby lepszy, gdyby kod wyglądał,

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

    jest równoważny, ale trudniejszy do wykrycia.

  3. Importowanie wielu klas - w projekcie Java, nad którym pracuję, obowiązuje zasada, że ​​importujemy tylko te klasy, z których faktycznie korzystamy; nigdy nie widzisz czegoś takiego

    import java.awt.*;

    w naszym kodzie źródłowym. Po zastosowaniu tej reguły często zdarza się, że kilkanaście instrukcji importowania pochodzi z tego samego pakietu. Jeśli dzieje się tak w twoim kodzie, może to być dobre miejsce do szukania ukrytych przykładów naruszeń. Jeśli musisz go zaimportować, jesteś z nim połączony. Jeśli to się zmieni, być może będziesz musiał to zrobić. Poprzez bezpośrednie zaimportowanie klas zaczniesz sprawdzać, jak naprawdę są połączone Twoje klasy.

Rozumiem, że twój przykład jest w języku Ruby, ale powinien mieć zastosowanie we wszystkich językach OOP.

Pan Polywhirl
źródło