OK, więc tytuł to trochę clickbaity, ale tak na serio, jestem na dobrej drodze , nie proś przez chwilę kick. Podoba mi się, jak zachęca metody do używania jako wiadomości w sposób zorientowany obiektowo. Ale to ma dokuczliwy problem, który grzechotał mi w głowie.
Zacząłem podejrzewać, że dobrze napisany kod może jednocześnie przestrzegać zasad OO i zasad funkcjonalnych. Staram się pogodzić te pomysły z wielkim punktem spornym, na którym wylądowałem return
.
Funkcja czysta ma dwie cechy:
Wielokrotne wywoływanie tego samego wejścia zawsze daje ten sam rezultat. Oznacza to, że jest niezmienny. Jego stan jest ustawiany tylko raz.
Nie powoduje żadnych skutków ubocznych. Jedyną zmianą wywołaną przez wywołanie tego jest wynik.
Jak więc przejść na temat bycia czysto funkcjonalnym, jeśli przysiągłeś, że używasz return
swojego sposobu komunikowania wyników?
Tell, nie pytaj prace pomysł za pomocą tego, co niektórzy uważają efekt uboczny. Kiedy mam do czynienia z przedmiotem, nie pytam go o jego stan wewnętrzny. Mówię mu, co muszę zrobić, i wykorzystuje swój stan wewnętrzny, aby dowiedzieć się, co zrobić z tym, co powiedziałem. Kiedy to powiem, nie pytam, co to zrobiło. Po prostu oczekuję, że coś to zrobiło z tym, co mu kazano.
Myślę, że Tell, Don't Ask to coś więcej niż inna nazwa enkapsulacji. Kiedy używam, return
nie mam pojęcia, co mnie wezwało. Nie mogę mówić, że to protokół, muszę go zmusić, by zajął się moim protokołem. Co w wielu przypadkach wyraża się jako stan wewnętrzny. Nawet jeśli to, co jest widoczne, nie jest dokładnie stanem, zwykle jest to tylko kilka obliczeń przeprowadzonych na argumencie stanu i wejściowym. Posiadanie interfejsu umożliwiającego reakcję daje szansę na zamaskowanie wyników w coś bardziej znaczącego niż stan wewnętrzny lub obliczenia. To jest przekazywanie wiadomości . Zobacz ten przykład .
Dawno temu, kiedy dyski faktycznie miały dyski, a napędem kciuka było to, co robiłeś w samochodzie, gdy koło było zbyt zimne, aby dotykać palcami, dowiedziałem się, jak denerwujące osoby uważają funkcje, które mają parametry. void swap(int *first, int *second)
wydawało się to bardzo przydatne, ale zachęcano nas do pisania funkcji, które zwracały wyniki. Wziąłem to sobie do serca z wiarą i zacząłem za tym podążać.
Ale teraz widzę ludzi budujących architektury, w których obiekty pozwalają, w jaki sposób zostały zbudowane, kontrolować, gdzie wysyłają swoje wyniki. Oto przykładowa implementacja . Wstrzyknięcie obiektu portu wyjściowego ponownie przypomina trochę pomysł parametru out. Ale w ten sposób obiekty „nie mów” pytają inne obiekty o tym, co zrobiły.
Kiedy po raz pierwszy dowiedziałem się o skutkach ubocznych, pomyślałem o tym jak o parametrze wyjściowym. Powiedziano nam, aby nie zaskakiwać ludzi tym, że niektóre prace odbywają się w zaskakujący sposób, to znaczy nie przestrzegając return result
konwencji. Teraz jestem pewien, że wiem, że jest stos równoległych problemów z asynchronicznym wątkiem, z którymi psują się skutki uboczne, ale powrót jest tak naprawdę tylko konwencją, w której pozostawiasz wynik wypchnięty na stos, więc niezależnie od tego, jak go nazwiesz, możesz go później usunąć. To wszystko, czym tak naprawdę jest.
O co tak naprawdę staram się zapytać:
Czy jest return
to jedyny sposób, aby uniknąć tych wszystkich niedoli związanych z efektami ubocznymi i uzyskać bezpieczeństwo nici bez zamków itp. Czy też mogę powiedzieć, nie pytać w czysto funkcjonalny sposób?
źródło
Odpowiedzi:
Jeśli funkcja nie wywołuje żadnych skutków ubocznych i niczego nie zwraca, oznacza to, że funkcja jest bezużyteczna. To takie proste.
Ale myślę, że możesz użyć niektórych kodów, jeśli chcesz postępować zgodnie z literą zasad i zignorować leżące u ich podstaw rozumowanie. Na przykład użycie parametru out oznacza, że nie używa on znaku powrotu. Ale nadal robi dokładnie to samo, co zwrot, tylko w bardziej zawiły sposób. Więc jeśli uważasz, że zwrot jest z jakiegoś powodu zły , to użycie parametru out jest wyraźnie złe z tych samych podstawowych powodów.
Możesz użyć bardziej skomplikowanych kodów. Np. Haskell jest znany ze sztuczki monadowej IO, w której możesz mieć skutki uboczne w praktyce, ale wciąż nie mówiąc ściśle, mają skutki uboczne z teoretycznego punktu widzenia. Kolejnym trikiem jest styl kontynuacji przekazywania, który pozwala uniknąć zwrotów w cenie zamiany kodu w spaghetti.
Najważniejsze jest to, że w przypadku braku głupich sztuczek dwie zasady funkcji wolnych od skutków ubocznych i „brak zwrotów” są po prostu niezgodne. Co więcej, zwrócę uwagę, że obie są naprawdę złymi zasadami (naprawdę dogmaty), ale to inna dyskusja.
Reguły takie jak „mów, nie pytaj” lub „żadnych skutków ubocznych” nie mogą być stosowane uniwersalnie. Zawsze musisz wziąć pod uwagę kontekst. Program bez skutków ubocznych jest dosłownie bezużyteczny. Nawet czyste języki funkcjonalne to potwierdzają. Starają się raczej oddzielić czyste części kodu od tych z efektami ubocznymi. Celem monad Stanowych lub IO w Haskell nie jest to, że unikasz skutków ubocznych - ponieważ nie możesz - ale to, że obecność efektów ubocznych jest wyraźnie oznaczona przez sygnaturę funkcji.
Zasada tell-dont-ask dotyczy innego rodzaju architektury - stylu, w którym obiekty w programie są niezależnymi „aktorami” komunikującymi się ze sobą. Każdy aktor jest zasadniczo autonomiczny i zamknięty. Możesz wysłać mu wiadomość, która decyduje, jak na nią zareagować, ale nie możesz zbadać wewnętrznego stanu aktora z zewnątrz. Oznacza to, że nie można stwierdzić, czy komunikat zmienia wewnętrzny stan aktora / obiektu. Stan i skutki uboczne są ukryte w projekcie .
źródło
Powiedz, nie pytaj zawiera kilka podstawowych założeń:
Żadna z tych rzeczy nie dotyczy czystych funkcji.
Sprawdźmy więc, dlaczego mamy zasadę „mów, nie pytaj”. Ta reguła jest ostrzeżeniem i przypomnieniem. Można to streścić w następujący sposób:
Innymi słowy, klasy ponoszą wyłączną odpowiedzialność za utrzymanie własnego stanu i działanie na nim. O to właśnie chodzi w enkapsulacji.
Od Fowlera :
Powtarzając, nic z tego nie ma nic wspólnego z funkcjami czystymi, a nawet nieczystymi, chyba że narażasz stan klasy na świat zewnętrzny. Przykłady:
Naruszenie TDA
To nie jest naruszenie TDA
lub
W obu ostatnich przykładach sygnalizacja świetlna zachowuje kontrolę nad swoim stanem i działaniami.
źródło
Nie pytasz tylko o jego stan wewnętrzny , nie pytasz też, czy ma w ogóle stan wewnętrzny .
Powiedz też , nie pytaj! nie nie oznacza, nie dostaję wynik w postaci wartości zwracanej (dostarczonych przez
return
oświadczenie wewnątrz metody). To po prostu sugeruje , że nie obchodzi mnie, jak to robisz, ale rób to! . A czasami chcesz natychmiast wynik przetwarzania ...źródło
next()
metoda iteratorów powinna nie tylko zwrócić bieżący obiekt, ale także zmienić stan wewnętrzny iteratorów, aby następne wywołanieJeśli uważasz, że
return
jest „szkodliwy” (aby pozostać na zdjęciu), zamiast tworzyć podobną funkcjęzbuduj go w sposób przekazujący wiadomości:
Tak długo, jak
f
ig
bez skutków ubocznych, łączenie ich ze sobą będzie również wolne od skutków ubocznych. Myślę, że ten styl jest podobny do tego, co jest również nazywane stylem Kontynuacja-przekazywanie .Jeśli to naprawdę prowadzi do „lepszych” programów, jest dyskusyjne, ponieważ łamie niektóre konwencje. Niemiecki inżynier oprogramowania Ralf Westphal stworzył wokół tego cały model programistyczny, nazwał go „komponentami opartymi na zdarzeniach”, stosując technikę modelowania, którą nazywa „Flow Design”.
Aby zobaczyć kilka przykładów, zacznij od sekcji „Tłumaczenie na wydarzenia” tego wpisu na blogu . Aby uzyskać pełne podejście, polecam jego e-book „Wiadomości jako model programowania - robienie OOP tak, jakbyś miał na myśli” .
źródło
If this really leads to "better" programs is debatable
Wystarczy spojrzeć na kod napisany w JavaScript w pierwszej dekadzie tego stulecia. Jquery i jego wtyczki były podatne na wywołania zwrotne tego paradygmatu ... wszędzie tam . W pewnym momencie zbyt wiele zagnieżdżonych wywołań zwrotnych sprawiło, że debugowanie stało się koszmarem. Kod nadal musi być czytany przez ludzi bez względu na dziwactwa inżynierii oprogramowania i jej „zasad”return
dane z funkcji i nadal stosować się do instrukcji Tell Don't Ask, o ile nie kontrolujesz stanu obiektu.Przekazywanie wiadomości jest z natury skuteczne. Jeśli powiesz obiektowi, aby coś zrobił , oczekujesz, że będzie to miało na coś wpływ. Jeśli moduł obsługi wiadomości był czysty, nie trzeba wysyłać mu wiadomości.
W systemach aktorów rozproszonych wynik operacji jest zwykle wysyłany jako wiadomość zwrotna do nadawcy pierwotnego żądania. Nadawca wiadomości jest domyślnie udostępniany przez środowisko wykonawcze aktora lub jest (zgodnie z konwencją) jawnie przekazywany jako część wiadomości. W synchronicznym przekazywaniu wiadomości pojedyncza odpowiedź jest podobna do
return
instrukcji. W asynchronicznym przekazywaniu komunikatów używanie komunikatów odpowiedzi jest szczególnie przydatne, ponieważ pozwala na równoczesne przetwarzanie wielu aktorów, przy jednoczesnym dostarczaniu wyników.Przekazywanie „nadawcy”, do którego wynik powinien zostać wyraźnie podany, zasadniczo modeluje styl przekazywania lub parametry budzące lęk - z wyjątkiem tego, że przekazuje im wiadomości zamiast mutować je bezpośrednio.
źródło
Całe to pytanie uderza mnie jako „naruszenie poziomu”.
Masz (przynajmniej) następujące poziomy w dużym projekcie:
I tak dalej, aż do poszczególnych tokenów.
Tak naprawdę nie ma potrzeby, aby jednostka na poziomie metody / funkcji nie zwracała (nawet jeśli po prostu zwraca
this
). I nie ma (w twoim opisie) potrzeby by podmiot na poziomie aktora zwrócił cokolwiek (w zależności od języka, który może nawet nie być możliwy). Myślę, że zamieszanie polega na połączeniu tych dwóch poziomów i argumentowałbym, że należy je wyraźnie uzasadnić (nawet jeśli dany obiekt faktycznie obejmuje wiele poziomów).źródło
Wspominasz, że chcesz zachować zgodność zarówno z zasadą OOP „powiedz, nie pytaj”, jak i zasadą funkcjonalną funkcji czystych, ale nie do końca rozumiem, w jaki sposób doprowadziło to do uniknięcia instrukcji return.
Względnie powszechnym alternatywnym sposobem postępowania zgodnie z obiema tymi zasadami jest wejście all-in na instrukcje return i używanie niezmiennych obiektów tylko z getterami. Podejście polega zatem na tym, że niektóre pobierające zwracają podobny obiekt z nowym stanem, w przeciwieństwie do zmiany stanu oryginalnego obiektu.
Jednym z przykładów tego podejścia jest wbudowane Python
tuple
ifrozenset
typy danych. Oto typowe użycie zestawu zamrożonego:Które wypisze następujące, pokazując, że metoda unii tworzy nowy zestaw z własnym stanem bez wpływu na stare obiekty:
Innym obszernym przykładem podobnych niezmiennych struktur danych jest biblioteka Immutable.js na Facebooku . W obu przypadkach zaczynasz od tych bloków konstrukcyjnych i możesz budować obiekty domeny wyższego poziomu, które działają zgodnie z tymi samymi zasadami, osiągając funkcjonalne podejście OOP, które pomaga łatwiej hermetyzować dane i uzasadniać je. Niezmienność pozwala również czerpać korzyść z możliwości udostępniania takich obiektów między wątkami bez martwienia się o zamki.
źródło
Staram się jak najlepiej pogodzić niektóre z korzyści, a konkretnie programowania imperatywnego i funkcjonalnego (oczywiście nie uzyskując wszystkich korzyści, ale staram się uzyskać lwią część obu), choć w
return
rzeczywistości jest to fundamentalne prosta moda w wielu przypadkach.Jeśli chodzi o próbę unikania
return
stwierdzeń wprost, przez ostatnią godzinę próbowałem się nad tym zastanowić i po prostu kilka razy przepełniłem mój mózg. Widzę jego atrakcyjność pod względem egzekwowania najsilniejszego poziomu enkapsulacji i ukrywania informacji na rzecz bardzo autonomicznych obiektów, które są po prostu mówione, co robić, lubię odkrywać skrajności pomysłów, choćby po to, aby spróbować uzyskać lepszy zrozumienie ich działania.Jeśli użyjemy przykładu sygnalizacji świetlnej, natychmiast naiwna próba chciałaby przekazać takiej wiedzy o całym świecie, który go otacza, a byłoby to z pewnością niepożądane z punktu widzenia łączenia. Więc jeśli dobrze rozumiem, streszczacie to i odsprzęgacie na rzecz uogólnienia koncepcji portów I / O, które dalej propagują komunikaty i żądania, a nie dane, poprzez potok, i zasadniczo wstrzykują te obiekty pożądanymi interakcjami / żądaniami między sobą nieświadomi siebie nawzajem.
Rurociąg węzłowy
I ten schemat jest o tyle, o ile próbowałem to naszkicować (i chociaż było to proste, musiałem ciągle go zmieniać i przemyślać). Natychmiast wydaje mi się, że projekt z takim poziomem oddzielenia i abstrakcji stałby się bardzo trudny do uzasadnienia w formie kodu, ponieważ orkiestrator (y), którzy łączą wszystkie te rzeczy w złożony świat, mogą mieć trudności z śledzić wszystkie te interakcje i żądania w celu stworzenia pożądanego potoku. Jednak w formie wizualnej może być dość proste, aby po prostu narysować te rzeczy jako wykres i połączyć wszystko i zobaczyć, jak rzeczy dzieją się interaktywnie.
Jeśli chodzi o skutki uboczne, widziałem, że jest to wolne od „skutków ubocznych” w tym sensie, że te żądania mogą, na stosie wywołań, prowadzić do łańcucha poleceń dla każdego wątku do wykonania, np. (Nie liczę tego jako „efekt uboczny” w sensie pragmatycznym, ponieważ nie zmienia on żadnego stanu istotnego dla świata zewnętrznego, dopóki takie polecenia nie zostaną faktycznie wykonane - praktycznym celem dla mnie w większości programów nie jest wyeliminowanie efektów ubocznych, ale odroczenie ich i scentralizowanie) . Co więcej, wykonanie polecenia może wygenerować nowy świat, zamiast mutować istniejący. Mój mózg jest naprawdę opodatkowany, po prostu próbując to wszystko zrozumieć, bez jakiejkolwiek próby prototypowania tych pomysłów. Ja też nie
Jak to działa
Aby wyjaśnić, wyobrażałem sobie, jak to właściwie programujesz. Sposób, w jaki widziałem, jak działa, był w rzeczywistości powyższym diagramem przechwytującym przepływ pracy użytkownika końcowego (programisty). Możesz przeciągnąć światłach na świat, przeciągnąć stoper, dać mu upływ czasu (po „zbudowaniu” go). Timer ma
On Interval
zdarzenie (port wyjściowy), możesz podłączyć to do sygnalizacji świetlnej, aby w takich przypadkach informowała światło, aby zmieniało kolory.Sygnalizacja świetlna może wtedy, po przełączeniu się na określone kolory, emitować sygnały wyjściowe (zdarzenia), takie jak,
On Red
w którym momencie możemy przeciągnąć pieszego do naszego świata i sprawić, że wydarzenie to nakazuje pieszemu zacząć chodzić ... lub możemy przeciągnąć ptaki do naszą scenę i spraw, aby gdy światło zmieniło kolor na czerwony, mówimy ptakom, aby zaczęły latać i trzepotać skrzydłami ... a może gdy światło zmieni kolor na czerwony, mówimy bomby, aby eksplodowała - cokolwiek chcemy, a obiekty są całkowicie nieświadomi siebie nawzajem i nie robiąc nic, tylko pośrednio mówiąc sobie, co robić poprzez tę abstrakcyjną koncepcję wejścia / wyjścia.I w pełni opisują swój stan i nie ujawniają niczego na ten temat (chyba że te „zdarzenia” są uważane za TMI, w którym to momencie musiałbym dużo przemyśleć rzeczy), mówią sobie nawzajem, że powinni robić pośrednio, nie pytają. I są niesamowicie oddzielone. Nic nie wie o niczym oprócz tej uogólnionej abstrakcji portów wejścia / wyjścia.
Praktyczne przypadki użycia?
Widziałem, że tego rodzaju rzeczy są przydatne jako język osadzony wysokiego poziomu specyficzny dla domeny w niektórych domenach do koordynowania wszystkich tych autonomicznych obiektów, które nie wiedzą nic o otaczającym świecie, nie ujawniają niczego z ich wewnętrznej budowy stanu postu i po prostu propagują żądania między sobą, które możemy zmieniać i dostosowywać do treści naszych serc. W tej chwili wydaje mi się, że jest to bardzo specyficzne dla danej dziedziny, a może po prostu nie zastanawiałem się nad tym, ponieważ bardzo trudno jest mi otoczyć mózg typami rzeczy, które regularnie rozwijam (często pracuję z raczej niski kod średniego poziomu), gdybym zinterpretował Tell, Don't Ask do takich skrajności i chcę jak najsilniejszego poziomu enkapsulacji, jaki można sobie wyobrazić. Ale jeśli pracujemy z abstrakcjami wysokiego poziomu w określonej domenie,
Sygnały i automaty
Ten projekt wydawał mi się dziwnie znajomy, dopóki nie zdałem sobie sprawy, że to w zasadzie sygnały i gniazda, jeśli nie weźmiemy pod uwagę wielu niuansów, w jaki sposób jest on uwzględniany. Głównym pytaniem jest dla mnie, jak skutecznie możemy zaprogramować te poszczególne węzły (obiekty) na wykresie jako ściśle przestrzegające
return
instrukcji Tell, Don't Ask, do stopnia unikania stwierdzeń i czy możemy ocenić wspomniany wykres bez mutacji (w równoległe, np. brak blokady). Właśnie tam magiczne korzyści nie polegają na tym, jak potencjalnie łączymy te rzeczy, ale jak można je wdrożyć do tego stopnia enkapsulacji przy braku mutacji. Oba te wydają mi się wykonalne, ale nie jestem pewien, jak szerokie byłoby to zastosowanie, i tutaj jestem trochę zakłopotany próbując przeanalizować potencjalne przypadki użycia.źródło
Tutaj wyraźnie widzę przeciek pewności. Wydaje się, że „efekt uboczny” jest dobrze znanym i powszechnie rozumianym terminem, ale w rzeczywistości tak nie jest. W zależności od twoich definicji (których faktycznie brakuje w PO), działania niepożądane mogą być całkowicie konieczne (jak udało się wyjaśnić @JacquesB) lub bezlitośnie nieakceptowane. Lub, robiąc krok w kierunku wyjaśnienia, konieczne jest rozróżnienie pożądanych efektów ubocznych, których nie lubi się ukrywać (w tym momencie pojawia się słynne IO Haskella: jest to tylko sposób na wyrażenie) i niepożądanych efektów ubocznych jako wynik błędów w kodzie i tego typu rzeczy . Są to dość różne problemy i dlatego wymagają innego rozumowania.
Proponuję więc zacząć od przeredagowania siebie: „Jak definiujemy efekt uboczny i co dane definicje mówią o jego związku z instrukcją„ return ”?”.
źródło