Jak wybrać między rozróżnieniem rozkazów Tell not Ask a Command Query?

25

Zasada Tell Don't Ask mówi:

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 opartych na stanie 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ę.

Prostym przykładem „Powiedz, nie pytaj” jest

Widget w = ...;
if (w.getParent() != null) {
  Panel parent = w.getParent();
  parent.remove(w);
}

a wersja tell to ...

Widget w = ...;
w.removeFromParent();

Ale co jeśli muszę znać wynik metody removeFromParent? Moja pierwsza reakcja polegała na zmianie metody removeFromParent na zwrócenie wartości logicznej oznaczającej, czy element nadrzędny został usunięty, czy nie.

Ale potem natknąłem się na wzorzec rozdzielania zapytań, który mówi NIE robić tego.

Stwierdza, że ​​każda metoda powinna być albo komendą wykonującą akcję, albo zapytaniem zwracającym dane do dzwoniącego, ale nie jednym i drugim. Innymi słowy, zadanie pytania nie powinno zmienić odpowiedzi. Bardziej formalnie, metody powinny zwracać wartość tylko wtedy, gdy są referencyjnie przejrzyste, a zatem nie mają żadnych skutków ubocznych.

Czy te dwie rzeczy naprawdę są ze sobą sprzeczne i jak wybrać między nimi? Czy korzystam z tego w Pragmatic Programmer lub Bertrand Meyer?

Dakotah North
źródło
1
co byś zrobił z wartością logiczną?
David
1
Wygląda na to, że
nurkujesz
Re boolean ... jest to przykład, który łatwo było odreagować, ale podobny do poniższej operacji zapisu, celem jest status operacji.
Dakotah North
2
Chodzi mi o to, że nie powinieneś skupiać się na „zwróceniu czegoś”, ale bardziej na tym, co chcesz z tym zrobić. Jak powiedziałem w innym komentarzu, jeśli skupiasz się na niepowodzeniu, użyj wyjątku, jeśli chcesz coś zrobić po zakończeniu operacji, a następnie użyj wywołania zwrotnego lub zdarzeń, jeśli chcesz zarejestrować, co się stało, to odpowiedzialność za widżet ... dlaczego pytam o twoje potrzeby. Jeśli nie możesz znaleźć konkretnego przykładu, w którym musisz coś zwrócić, być może oznacza to, że nie będziesz musiał wybierać między tymi dwoma.
David,
1
Rozdzielanie zapytań poleceń jest zasadą, a nie wzorem. Wzory są czymś, czego można użyć do rozwiązania problemu. Przestrzegasz zasad, aby nie stwarzać problemów.
candied_orange

Odpowiedzi:

25

W rzeczywistości twój przykładowy problem ilustruje już brak rozkładu.

Zmieńmy to trochę:

Book b = ...;
if (b.getShelf() != null) 
    b.getShelf().remove(b);

Tak naprawdę nie jest inaczej, ale uwidacznia się wada: dlaczego książka miałaby wiedzieć o swojej półce? Mówiąc wprost, nie powinno. Wprowadza zależność książek na półkach (co nie ma sensu) i tworzy okrągłe odniesienia. Wszystko jest źle.

Podobnie widżet nie musi znać swojego elementu nadrzędnego. Powiesz: „Ok, ale widżet potrzebuje swojego elementu nadrzędnego, aby sam układ się poprawnie itp.” i pod maską widżet zna swojego rodzica i prosi go o obliczenia własnych metryk na ich podstawie itp. Mówiąc, nie pytaj, że to źle. Rodzic powinien powiedzieć wszystkim swoim dzieciom, aby renderowały, przekazując wszystkie niezbędne informacje jako argumenty. W ten sposób można łatwo mieć ten sam widget w dwóch rodzicach (niezależnie od tego, czy ma to sens).

Aby wrócić do przykładu książki - wpisz bibliotekarza:

Librarian l = ...;
Book b = ...;
l.findShelf(b).remove(b);

Proszę zrozumieć, że nie pytamy już w sensie powiedzieć, nie pytaj .

W pierwszej wersji półka była własnością książki i poprosiłeś o nią. W drugiej wersji nie zakładamy takiego założenia na temat książki. Wiemy tylko, że bibliotekarz może nam powiedzieć półkę z książką. Przypuszczalnie opiera się na jakimś rodzaju tabeli przeglądowej, ale nie jesteśmy tego pewni (mógł po prostu przeglądać wszystkie książki na każdej półce) i tak naprawdę nie chcemy tego wiedzieć .
Nie polegamy na tym, czy lub jak odpowiedź bibliotekarzy jest sprzężona z jej stanem lub ze stanem innych obiektów, od których może zależeć. Mówimy bibliotekarzowi, żeby znalazł półkę.
W pierwszym scenariuszu bezpośrednio zakodowaliśmy relację między książką a półką jako część stanu książki i pobraliśmy ten stan bezpośrednio. Jest to bardzo kruche, ponieważ przyjmujemy również założenie, że półka zwrócona przez książkę zawiera książkę (jest to ograniczenie, które musimy zapewnić, w przeciwnym razie możemy nie być w stanie wydać removeksiążki z półki, w której się znajduje).

Wraz z wprowadzeniem bibliotekarza modelujemy ten związek osobno, osiągając w ten sposób rozdzielenie obaw.

back2dos
źródło
Myślę, że to bardzo ważna kwestia, ale w ogóle nie odpowiada na pytanie. W twojej wersji pytanie brzmi: czy metoda remove (Book b) w klasie Shelf powinna mieć wartość zwracaną?
scarfridge
1
@ scarfridge: Mówiąc w skrócie , nie pytaj jest następstwem właściwego rozdzielenia obaw. Jeśli się nad tym zastanowić, odpowiadam na pytanie. Przynajmniej tak mi się wydaje;)
back2dos
1
@ scarfridge Zamiast wartości zwracanej można przekazać funkcję / delegata wywoływaną w przypadku niepowodzenia. Jeśli się powiedzie, skończysz, prawda?
Pobłogosław Yahu
2
@BlessYahu To wydaje mi się zbyt skomplikowane. Osobiście uważam, że separacja poleceń i poleceń jest bardziej idealna w prawdziwym (wielowątkowym) świecie. IMHO jest w porządku, aby metoda z efektami ubocznymi zwracała wartość, o ile nazwa metody wyraźnie wskazuje, że zmieni ona stan obiektu. Rozważ metodę pop () stosu. Ale metoda z nazwą zapytania nie powinna mieć skutków ubocznych.
scarfridge
13

Jeśli potrzebujesz znać wynik, to robisz; to twoje wymaganie.

Wyrażenie „metody powinny zwracać wartość tylko wtedy, gdy są referencyjnie przezroczyste, a zatem nie mają skutków ubocznych” jest dobrą wskazówką do naśladowania (szczególnie jeśli piszesz swój program w funkcjonalnym stylu, ze względu na współbieżność lub z innych powodów), ale jest to nie absolut.

Na przykład możesz potrzebować znać wynik operacji zapisu pliku (prawda lub fałsz). To przykład metody, która zwraca wartość, ale zawsze wywołuje skutki uboczne; nie można tego obejść.

Aby wykonać rozdzielanie poleceń / kwerend, należy wykonać operację na pliku za pomocą jednej metody, a następnie sprawdzić jej wynik za pomocą innej metody, co jest niepożądaną techniką, ponieważ oddziela wynik od metody, która spowodowała wynik. Co się stanie, jeśli coś stanie się ze stanem obiektu między wywołaniem pliku a sprawdzeniem stanu?

Najważniejsze jest to: jeśli używasz wyniku wywołania metody do podejmowania decyzji w innym miejscu w programie, nie naruszasz zasad Tell Don't Ask. Jeśli natomiast podejmujesz decyzje dotyczące obiektu na podstawie wywołania metody do tego obiektu, powinieneś przenieść te decyzje do samego obiektu, aby zachować enkapsulację.

Nie jest to wcale sprzeczne z separacją zapytań dowodzenia; w rzeczywistości dodatkowo to egzekwuje, ponieważ nie ma już potrzeby ujawniania zewnętrznej metody dla celów statusu.

Robert Harvey
źródło
1
Można argumentować, że w twoim przykładzie, jeśli operacja „zapisu” zakończy się niepowodzeniem, należy zgłosić wyjątek, więc nie ma potrzeby stosowania metody „get status”.
David
@David: Następnie powróć do przykładu OP, który zwróciłby prawdę, gdyby rzeczywiście nastąpiła zmiana, fałsz, jeśli tak się nie stało. Wątpię, byś chciał wprowadzić tam wyjątek.
Robert Harvey,
Nawet przykład OP nie jest dla mnie odpowiedni. Albo chcesz być zauważony, jeśli usunięcie nie było możliwe, w takim przypadku skorzystasz z wyjątku lub chcesz być zauważony, gdy widżet zostanie faktycznie usunięty, w którym to przypadku możesz użyć mechanizmu zdarzenia lub wywołania zwrotnego. Po prostu nie widzę prawdziwego przykładu, w którym nie chciałbyś uszanować „Wzorca rozdzielania zapytań”
David,
@David: Zredagowałem swoją odpowiedź, aby wyjaśnić.
Robert Harvey,
Nie wspominając o tym zbyt dobrze, ale cała idea stojąca za separacją komend i zapytań polega na tym, że ktokolwiek wyda komendę, aby zapisać plik , nie dba o wynik - przynajmniej nie w określonym znaczeniu (użytkownik może później wyciągnij listę wszystkich ostatnich operacji na plikach, ale nie ma to związku z początkową komendą). Jeśli musisz podjąć działania po zakończeniu polecenia, bez względu na to, czy zależy ci na wyniku, poprawnym sposobem jest skorzystanie z asynchronicznego modelu programowania z wykorzystaniem zdarzeń, które (mogą) zawierać szczegóły dotyczące pierwotnego polecenia i / lub jego wynik.
Aaronaught
2

Myślę, że powinieneś iść ze swoim początkowym instynktem. Czasami projekt powinien być celowo niebezpieczny, aby wprowadzić złożoność natychmiast po komunikowaniu się z przedmiotem, aby wymuszone obchodzenie się z nim właściwie od razu, zamiast próbować obsługiwać go na samym obiekcie i ukryć wszystkie krwawe szczegóły i utrudnić czyste zmienić podstawowe założenia.

Powinieneś poczuć tonięcie, jeśli obiekt oferuje zaimplementowane odpowiedniki openi closezachowania, i natychmiast zaczynasz rozumieć, z czym masz do czynienia, gdy zobaczysz wartość logiczną zwrotu dla czegoś, co Twoim zdaniem byłoby prostym zadaniem atomowym.

Jak sobie z tym poradzisz, to twoja sprawa. Możesz utworzyć nad nim abstrakcję, typ widgetu removeFromParent(), ale zawsze powinieneś mieć rezerwę na niskim poziomie, w przeciwnym razie prawdopodobnie przyjmujesz przedwczesne założenia.

Nie staraj się, aby wszystko było proste. Nie ma nic bardziej rozczarowującego, gdy polegasz na czymś, co wydaje się być eleganckie i tak niewinne, tylko po to, aby uświadomić sobie, że to prawdziwy horror w najgorszym momencie.

Filip Dupanović
źródło
2

To, co opisujesz, jest znanym „wyjątkiem” od zasady rozdzielania zapytań i poleceń.

W tym artykule Martin Fowler wyjaśnia, jak zagroziłby takiemu wyjątkowi.

Meyer lubi bezwzględnie stosować separację poleceń i zapytań, ale są wyjątki. Dobranie stosu jest dobrym przykładem zapytania, które modyfikuje stan. Meyer słusznie twierdzi, że można uniknąć tej metody, ale jest to przydatny idiom. Dlatego wolę stosować tę zasadę, kiedy mogę, ale jestem gotów ją złamać, aby uzyskać pop.

W twoim przykładzie rozważę ten sam wyjątek.

elviejo79
źródło
0

Rozdzielenie poleceń i zapytań jest niezwykle łatwe do niezrozumienia.

Jeśli powiem ci w moim systemie, istnieje polecenie, które również zwraca wartość z zapytania i mówisz: „Ha! Naruszasz!” skaczesz z pistoletu.

Nie, to nie jest zabronione.

Zabronione jest, gdy to polecenie jest JEDYNYM sposobem wykonania tego zapytania. Nie. Nie powinienem zmieniać stanu, aby zadawać pytania. To nie znaczy, że muszę zamykać oczy za każdym razem, gdy zmieniam stan.

Nie ma znaczenia, czy to zrobię, ponieważ i tak zamierzam je ponownie otworzyć. Jedyną korzyścią z tej nadmiernej reakcji było to, że projektanci nie byli leniwi i nie zapomnieli dołączyć zapytania bez zmiany stanu.

Prawdziwe znaczenie jest tak cholernie subtelne, że leniwym ludziom łatwiej jest powiedzieć, że rozgrywający powinni powrócić. Ułatwia to upewnienie się, że nie naruszasz prawdziwej reguły, ale jest to nadmierna reakcja, która może stać się prawdziwym bólem.

Płynne interfejsy i iDSL ciągle „naruszają” tę nadmierną reakcję. Jeśli zareagujesz nadmiernie, zignorujesz wiele mocy.

Możesz argumentować, że polecenie powinno robić tylko jedną rzecz, a zapytanie powinno robić tylko jedną rzecz. I w wielu przypadkach jest to dobry punkt. Kto powiedział, że jest tylko jedno zapytanie, które powinno być zgodne z poleceniem? Dobrze, ale to nie jest separacja poleceń i zapytań. To zasada pojedynczej odpowiedzialności.

Patrząc w ten sposób, pop stos nie jest jakimś dziwnym wyjątkiem. To jedna odpowiedzialność. Pop narusza separację zapytań, jeśli zapomnisz podać podgląd.

candied_orange
źródło