Wyjaśnienie, w jaki sposób „Powiedz, nie pytaj” jest uważane za dobrego OO

49

Ten blog został opublikowany w Hacker News z kilkoma pozytywnymi opiniami. Pochodząc z C ++, większość tych przykładów wydaje się sprzeczna z tym, czego mnie nauczono.

Tak jak w przykładzie 2:

Zły:

def check_for_overheating(system_monitor)
  if system_monitor.temperature > 100
    system_monitor.sound_alarms
  end
end

kontra dobry:

system_monitor.check_for_overheating

class SystemMonitor
  def check_for_overheating
    if temperature > 100
      sound_alarms
    end
  end
end

Rada w C ++ jest taka, że ​​powinieneś preferować wolne funkcje zamiast funkcji składowych, ponieważ zwiększają one enkapsulację. Oba są identyczne semantycznie, więc dlaczego preferować wybór, który ma dostęp do większej liczby stanów?

Przykład 4:

Zły:

def street_name(user)
  if user.address
    user.address.street_name
  else
    'No street name on file'
  end
end

kontra dobry:

def street_name(user)
  user.address.street_name
end

class User
  def address
    @address || NullAddress.new
  end
end

class NullAddress
  def street_name
    'No street name on file'
  end
end

Dlaczego Userformatowanie niepowiązanego ciągu błędów jest obowiązkiem ? Co jeśli chcę zrobić coś poza drukowaniem, 'No street name on file'jeśli nie ma ulicy? Co jeśli ulica ma taką samą nazwę?


Czy ktoś mógłby mnie oświecić na temat zalet i uzasadnienia „Powiedz, nie pytaj”? Nie szukam tego, co jest lepsze, ale próbuję zrozumieć punkt widzenia autora.

Pubby
źródło
Przykładami kodu mogą być Ruby, a nie Python, nie wiem.
Pubby
2
Zawsze zastanawiam się, czy coś takiego jak pierwszy przykład nie jest raczej naruszeniem SRP?
stijn
1
Możesz przeczytać: pragprog.com/articles/tell-dont-ask
Mik378
Rubin. @ jest na przykład skrótem, a Python niejawnie kończy swoje bloki spacją.
Erik Reppen
3
„Rada w C ++ jest taka, że ​​powinieneś preferować wolne funkcje zamiast funkcji członkowskich, ponieważ zwiększają one enkapsulację.” Nie wiem, kto ci to powiedział, ale to nieprawda. Darmowe funkcje mogą być użyte do zwiększenia enkapsulacji, ale niekoniecznie zwiększają enkapsulację.
Rob K

Odpowiedzi:

81

Pytanie obiektu o jego stan, a następnie wywołanie metod na tym obiekcie na podstawie decyzji podejmowanych poza nim oznacza, że ​​obiekt jest teraz nieszczelną abstrakcją; niektóre z jego zachowań znajdują się poza obiektem, a stan wewnętrzny jest narażony (być może niepotrzebnie) na świat zewnętrzny.

Powinieneś starać się powiedzieć obiektom, co chcesz, aby zrobiły; nie zadawaj im pytań o ich stan, podejmuj decyzje, a następnie mów im, co mają robić.

Problem polega na tym, że jako osoba wywołująca nie powinna podejmować decyzji na podstawie stanu wywoływanego obiektu, które powodują zmianę stanu obiektu. Logika, którą wdrażasz, jest prawdopodobnie odpowiedzialnością wywoływanego obiektu, a nie twoją. Podejmowanie decyzji poza obiektem narusza jego enkapsulację.

Pewnie, możesz powiedzieć, to oczywiste. Nigdy nie napisałbym takiego kodu. Mimo to bardzo łatwo jest uśpić się badaniem obiektu, do którego istnieje odniesienie, a następnie wywoływaniem różnych metod na podstawie wyników. Ale to może nie być najlepszy sposób na zrobienie tego. Powiedz obiektowi, co chcesz. Niech wymyśli, jak to zrobić. Myśl deklaratywnie zamiast proceduralnie!

Łatwiej jest uniknąć tej pułapki, jeśli zaczniesz od zaprojektowania zajęć na podstawie ich obowiązków; następnie możesz naturalnie przejść do określania poleceń, które klasa może wykonywać, w przeciwieństwie do zapytań, które informują o stanie obiektu.

http://pragprog.com/articles/tell-dont-ask

Robert Harvey
źródło
4
Przykładowy tekst zabrania wielu rzeczy, które są wyraźnie dobrą praktyką.
DeadMG,
13
@DeadMG robi to, co mówisz, tylko tym, którzy podążają za nim niewolniczo, którzy ślepo ignorują słowo „pragmatyczne” w nazwie strony i kluczowej myśli autorów strony , co zostało wyraźnie powiedziane w ich kluczowej książce: „nie ma czegoś takiego jak najlepsze rozwiązanie ... ”
komar
2
Nigdy nie czytaj książki. Nie chciałbym też. Czytam tylko przykładowy tekst, który jest całkowicie sprawiedliwy.
DeadMG,
3
@DeadMG nie martw się. Teraz, gdy znasz kluczową kwestię, która umieszcza ten przykład (i każdy inny z pragprogu w tej sprawie) w zamierzonym kontekście („nie ma czegoś takiego jak najlepsze rozwiązanie ...”), nie można czytać książki
gnat
1
Nadal nie jestem pewien, co powinien powiedzieć dla Ciebie komunikat Tell, Don't Ask bez kontekstu, ale jest to naprawdę dobra rada OOP.
Erik Reppen
16

Ogólnie rzecz biorąc, artykuł sugeruje, że nie powinieneś narażać państwa członkowskiego na przemawianie przez innych, jeśli możesz sam o tym myśleć .

Jednak nie jest jednoznacznie stwierdzone, że prawo to mieści się w bardzo oczywistych granicach, gdy rozumowanie wykracza poza zakres odpowiedzialności określonej klasy. Na przykład, każda klasa, której zadaniem jest utrzymanie pewnej wartości lub dostarczenie pewnej wartości - szczególnie ogólnej, lub w której klasa zapewnia zachowanie, które należy rozszerzyć.

Na przykład, jeśli system podaje temperaturezapytanie, to jutro klient może to zrobić check_for_underheatingbez konieczności zmiany SystemMonitor. Nie dzieje się tak w przypadku samych SystemMonitornarzędzi check_for_overheating. Zatem SystemMonitorklasa, której zadaniem jest wzbudzanie alarmu, gdy temperatura jest zbyt wysoka, postępuje zgodnie z tym - ale SystemMonitorklasa, której zadaniem jest umożliwienie innemu kodowi odczytania temperatury, aby mógł kontrolować, powiedzmy, TurboBoost lub coś w tym rodzaju , nie powinieneś.

Zauważ również, że drugi przykład bezcelowo wykorzystuje anty-wzorzec Null Object.

DeadMG
źródło
19
„Obiekt zerowy” nie jest tym, co nazwałbym anty-wzorem, więc zastanawiam się, jaki jest tego powód?
Konrad Rudolph,
4
Jestem prawie pewien, że nikt nie ma metod określonych jako „Nie robi nic”. To sprawia, że ​​nazywanie ich jest trochę bezcelowe. Oznacza to, że każdy obiekt implementujący Null Object przynajmniej łamie LSP i opisuje siebie jako implementujący operacje, których tak naprawdę nie robi. Użytkownik oczekuje wartości z powrotem. Od tego zależy poprawność ich programu. Po prostu przynosisz więcej problemów, udając, że jest to wartość, gdy tak nie jest. Czy kiedykolwiek próbowałeś debugować ciche metody, które zawiodły? Jest to niemożliwe i nikt nie powinien przez to cierpieć.
DeadMG,
4
Twierdziłbym, że to całkowicie zależy od dziedziny problemów.
Konrad Rudolph,
5
@DeadMG Zgadzam się, że powyższy przykład jest zły korzystanie z wzoru Null Object, ale nie jest to zasługa aby go używać. Kilka razy użyłem implementacji „no-op” jakiegoś interfejsu lub innego, aby uniknąć sprawdzania wartości zerowej lub przepuszczania przez system prawdziwej wartości zerowej.
Maks.
6
Nie jestem pewien, czy rozumiem twój punkt widzenia, gdy „klient może to zrobić check_for_underheatingbez konieczności zmiany SystemMonitor”. Czym różni się klient od SystemMonitortego momentu? Czy nie rozpraszasz teraz logiki monitorowania na wiele klas? Nie widzę też problemu z klasą monitora, która dostarcza informacje z czujnika innym klasom, jednocześnie rezerwując sobie funkcje alarmowe. Kontroler doładowania powinien sterować doładowaniem bez konieczności martwienia się o podniesienie alarmu, jeśli temperatura będzie zbyt wysoka.
TMN
9

Prawdziwy problem z twoim przykładem przegrzania polega na tym, że reguły dotyczące tego, co kwalifikuje się jako przegrzanie, nie są łatwo zmieniane dla różnych systemów. Załóżmy, że system A jest taki, jaki masz (temperatura> 100 się przegrzewa), ale system B jest bardziej delikatny (temperatura> 93 się przegrzewa). Czy zmieniasz funkcję sterowania, aby sprawdzić typ systemu, a następnie zastosujesz prawidłową wartość?

if (system is a System_A and system_monitor.temp >100)
  system_monitor.sound_alarms
else if (system is a System_B and system_monitor.temp > 93)
  system_monitor.sound_alarms
end

Czy może każdy typ systemu określa jego moc grzewczą?

EDYTOWAĆ:

system.check_for_overheating

class SystemA : System
  def check_for_overheating
    if temperature > 100
      sound_alarms
    end
  end
end

class SystemB : System
  def check_for_overheating
    if temperature > 93
      sound_alarms
    end
  end
end

Ten pierwszy sposób sprawia, że ​​funkcja kontrolna staje się brzydka, gdy zaczynasz radzić sobie z większą liczbą systemów. Ta ostatnia pozwala na stabilne działanie funkcji sterowania w miarę upływu czasu.

Matthew Flynn
źródło
1
Dlaczego nie zarejestrować każdego systemu na monitorze. Podczas rejestracji mogą wskazać, kiedy nastąpi przegrzanie.
Martin York,
@LokiAstari - Możesz, ale potem możesz spotkać nowy system, który jest również wrażliwy na wilgoć lub ciśnienie atmosferyczne. Zasadą jest wyodrębnienie tego, co się różni - w tym przypadku jest to podatność na przegrzanie
Matthew Flynn
1
Właśnie dlatego powinieneś mieć model tell. Poinformujesz system o bieżących warunkach i poinformuje Cię, czy wykracza poza normalne warunki pracy. W ten sposób nigdy nie musisz modyfikować SystemMoniter. To dla ciebie kapsułkowanie.
Martin York,
@LokiAstari - Myślę, że rozmawiamy tutaj o różnych celach - naprawdę chciałem stworzyć różne systemy, a nie różne monitory. Chodzi o to, że system powinien wiedzieć, kiedy jest w stanie, który wywołuje alarm, w przeciwieństwie do niektórych zewnętrznych funkcji kontrolera. SystemA powinien mieć swoje kryteria, SystemB powinien mieć własne. Kontroler powinien być w stanie zapytać (w regularnych odstępach czasu), czy system działa prawidłowo, czy nie.
Matthew Flynn
6

Po pierwsze, czuję, że muszę zrobić wyjątek od twojej charakterystyki przykładów jako „złych” i „dobrych”. W artykule użyto terminów „Nie tak dobrze” i „Lepiej”. Sądzę, że zostały one wybrane z jakiegoś powodu: są to wytyczne i zależnie od okoliczności podejście „Nie tak dobrze” może być właściwe, a nawet jedyne rozwiązanie.

Kiedy masz wybór, powinieneś preferować włączenie jakiejkolwiek funkcjonalności, która opiera się wyłącznie na klasie w klasie, a nie poza nią - przyczyną jest hermetyzacja i fakt, że ułatwia to ewolucję klasy w czasie. Klasa lepiej radzi sobie także z reklamowaniem swoich możliwości niż kilka bezpłatnych funkcji.

Czasami musisz powiedzieć, ponieważ decyzja zależy od czegoś spoza klasy lub ponieważ jest to po prostu coś, czego nie chcesz robić od większości użytkowników klasy. Czasami chcesz powiedzieć, ponieważ zachowanie jest sprzeczne z intuicją dla klasy i nie chcesz mylić większości użytkowników klasy.

Na przykład narzekasz na to, że adres ulicy zwraca komunikat o błędzie, to nie jest to, co robi, to podać wartość domyślną. Ale czasami wartość domyślna jest nieodpowiednia. Jeśli był to stan lub miasto, możesz chcieć przyjąć wartość domyślną podczas przypisywania rekordu sprzedawcy lub ankiecie, aby wszystkie niewiadome trafiły do ​​konkretnej osoby. Z drugiej strony, jeśli drukujesz koperty, możesz preferować wyjątek lub ochronę, która nie pozwala marnować papieru na listy, których nie można dostarczyć.

Mogą się zdarzyć przypadki, w których „Nie tak dobrze” jest dobrym rozwiązaniem, ale ogólnie „Lepsze” jest, cóż, lepsze.

jmoreno
źródło
3

Antymetria danych / obiektów

Jak zauważyli inni, Tell-Dont-Ask jest szczególnie przeznaczony do przypadków, w których zmieniasz stan obiektu po zapytaniu (patrz np. Tekst Pragprog opublikowany gdzie indziej na tej stronie). Nie zawsze tak jest, np. Obiekt „użytkownik” nie jest zmieniany po zapytaniu o adres user.address. Dlatego jest dyskusyjne, czy jest to odpowiedni przypadek zastosowania Tell-Dont-Ask.

Tell-Dont-Ask zajmuje się odpowiedzialnością, a nie wyciąganiem logiki z klasy, która powinna być w niej uzasadniona. Ale nie cała logika, która dotyczy obiektów, musi być logiką tych obiektów. Jest to wskazówka na głębszym poziomie, nawet poza Tell-Dont-Ask, i chcę dodać krótką uwagę na ten temat.

W ramach projektu architektonicznego możesz chcieć mieć obiekty, które są tak naprawdę tylko kontenerami dla właściwości, a może nawet niezmiennymi, a następnie uruchamiać różne funkcje nad kolekcjami takich obiektów, oceniając, filtrując lub przekształcając je zamiast wysyłać im polecenia (co jest więcej domeny Tell-Dont-Ask).

Wybór bardziej odpowiedni dla twojego problemu zależy od tego, czy spodziewasz się stabilnych danych (obiektów deklaratywnych), ale ze zmianą / dodaniem po stronie funkcji. Lub jeśli oczekujesz stabilnego i ograniczonego zestawu takich funkcji, ale oczekujesz większego strumienia na poziomie obiektów, np. Poprzez dodanie nowych typów. W pierwszej sytuacji wolisz funkcje wolne, w metodach drugiego obiektu.

Bob Martin w swojej książce „Clean Code” nazywa to „antysymetrią danych / obiektów” (s. 95ff), inne społeczności mogą nazywać to „ problemem ekspresji ”.

ThomasH
źródło
3

Ten paradygmat jest czasem określany jako „powiedz, nie pytaj” , co oznacza, że ​​powiedz obiektowi, co ma robić, nie pytaj o jego stan; a czasem jako „Zapytaj, nie mów” , co oznacza, że ​​poproś obiekt, aby coś dla ciebie zrobił, nie mów mu, jaki powinien być jego stan. W obu przypadkach najlepsza praktyka jest taka sama - sposób, w jaki obiekt powinien wykonać akcję, dotyczy troski o obiekt, a nie obiektu wywołującego. Interfejsy powinny unikać ujawniania swojego stanu (np. Poprzez akcesoria lub właściwości publiczne), a zamiast tego ujawniać metody „robienia”, których implementacja jest nieprzejrzysta. Inni opisali to linkami do pragmatycznego programisty.

Ta reguła jest związana z regułą dotyczącą unikania kodu „podwójnej kropki” lub „podwójnej strzałki”, często nazywanego „Rozmawiaj tylko z najbliższymi przyjaciółmi”, który foo->getBar()->doSomething()to stan jest zły, zamiast tego użyj funkcji, foo->doSomething();która jest funkcją zawijania opakowania wokół paska, i wdrożony jako prosty return bar->doSomething();- jeśli foojest odpowiedzialny za zarządzanie bar, niech to zrobi!

Nicholas Shanks
źródło
1

Oprócz innych dobrych odpowiedzi na temat „mów, nie pytaj”, komentarz do konkretnych przykładów, który może pomóc:

Rada w C ++ jest taka, że ​​powinieneś preferować wolne funkcje zamiast funkcji składowych, ponieważ zwiększają one enkapsulację. Oba są identyczne semantycznie, więc dlaczego preferować wybór, który ma dostęp do większej liczby stanów?

Ten wybór nie ma dostępu do więcej stanu. Obaj używają tej samej ilości stanu do wykonywania swoich zadań, ale „zły” przykład wymaga, aby państwo klasy było publiczne, aby wykonywać swoją pracę. Ponadto zachowanie tej klasy w „złym” przykładzie rozciąga się na funkcję bezpłatną, co utrudnia jej znalezienie i utrudnia refaktoryzację.

Dlaczego obowiązkiem użytkownika jest sformatowanie niepowiązanego ciągu błędów? Co jeśli chcę zrobić coś oprócz drukowania „Brak nazwy ulicy w pliku”, jeśli nie ma ulicy? Co jeśli ulica ma taką samą nazwę?

Dlaczego „nazwa ulicy” jest obowiązkiem zarówno „uzyskać nazwę ulicy”, jak i „podać komunikat o błędzie”? Przynajmniej w „dobrej” wersji każdy kawałek ma jedną odpowiedzialność. Mimo to nie jest to świetny przykład.

Telastyn
źródło
2
To nieprawda. Zakładasz, że sprawdzenie przegrzania jest jedyną rozsądną rzeczą związaną z temperaturą. Co jeśli klasa ma być jednym z wielu monitorów temperatury, a system musi podjąć różne działania, na przykład w zależności od wielu wyników? Jeśli to zachowanie można ograniczyć do wstępnie zdefiniowanego zachowania pojedynczej instancji, to na pewno. W przeciwnym razie oczywiście nie można go zastosować.
DeadMG,
Jasne, czy termostat i alarmy istniały w różnych klasach (tak jak powinny).
Telastyn
1
@DeadMG: Ogólna rada jest taka, aby uczynić rzeczy prywatnymi / chronionymi, dopóki nie będziesz mieć do nich dostępu. Chociaż ten konkretny przykład to meh, nie podważa to standardowej praktyki.
Guvante,
Przykład w artykule na temat tego, że praktyka jest „meh”, podważa to. Jeśli ta praktyka jest standardowa ze względu na wielkie korzyści, to po co kłopot ze znalezieniem odpowiedniego przykładu?
Stijn de Witt
1

Te odpowiedzi są bardzo dobre, ale oto kolejny przykład, aby podkreślić: pamiętaj, że zwykle jest to sposób na uniknięcie powielania. Załóżmy na przykład, że masz kilka miejsc z kodem takim jak:

Product product = productMgr.get(productUuid)
if (product.userUuid != currentUser.uuid) {
    throw BlahException("This product doesn't belong to this user")
}

Oznacza to, że lepiej mieć coś takiego:

Product product = productMgr.get(productUuid, currentUser)

Ponieważ to duplikowanie oznacza, że ​​większość klientów interfejsu użyłaby nowej metody, zamiast powtarzać tę samą logikę tu i tam. Dajesz swojemu delegatowi pracę, którą chcesz wykonać, zamiast prosić o informacje potrzebne do zrobienia tego sam.

antonio.fornie
źródło
0

Uważam, że jest to bardziej prawdziwe przy pisaniu obiektów wysokiego poziomu, ale mniej prawdziwe przy schodzeniu do głębszego poziomu, np. Biblioteki klas, ponieważ niemożliwe jest napisanie każdej metody, aby zadowolić wszystkich konsumentów klas.

Na przykład # 2, myślę, że jest zbyt uproszczony. Gdybyśmy rzeczywiście zamierzali to zaimplementować, SystemMonitor miałby w końcu kod dla niskiego poziomu dostępu do sprzętu i logiki dla abstrakcji na wysokim poziomie osadzony w tej samej klasie. Niestety, jeśli spróbujemy podzielić to na dwie klasy, naruszylibyśmy samo „Powiedz, nie pytaj”.

Przykład nr 4 jest mniej więcej taki sam - osadza logikę interfejsu użytkownika w warstwie danych. Teraz, jeśli chcemy naprawić to, co użytkownik chce zobaczyć w przypadku braku adresu, musimy naprawić obiekt w warstwie danych, a co jeśli dwa projekty używające tego samego obiektu, ale muszą użyć innego tekstu dla adresu zerowego?

Zgadzam się, że gdybyśmy mogli wdrożyć „Powiedz, nie pytaj” o wszystko, byłoby to bardzo przydatne - sam byłbym szczęśliwy, gdybym mógł raczej powiedzieć niż poprosić (i zrobić to sam) w prawdziwym życiu! Jednak, podobnie jak w prawdziwym życiu, wykonalność rozwiązania jest bardzo ograniczona do klas wysokiego poziomu.

tia
źródło