Czy naprawdę możemy zastosować niezmienność w OOP bez utraty wszystkich kluczowych funkcji OOP?


11

Widzę korzyści płynące z uczynienia obiektów w moim programie niezmiennymi. Kiedy naprawdę głęboko zastanawiam się nad dobrym projektem mojej aplikacji, często naturalnie dochodzę do tego, że wiele moich obiektów jest niezmiennych. Często dochodzi do tego, że chciałbym, aby wszystkie moje obiekty były niezmienne.

To pytanie dotyczy tego samego pomysłu, ale żadna odpowiedź nie sugeruje, jakie jest dobre podejście do niezmienności i kiedy z niego skorzystać. Czy istnieją jakieś niezmienne niezmienne wzory? Ogólną ideą wydaje się być „uczynienie obiektów niezmiennymi, chyba że absolutnie potrzebujesz ich do zmiany”, co w praktyce jest bezużyteczne.

Z mojego doświadczenia wynika, że ​​niezmienność coraz bardziej popycha mój kod do paradygmatu funkcjonalnego i ten postęp zawsze ma miejsce:

  1. Zaczynam potrzebować trwałych (w sensie funkcjonalnym) struktur danych, takich jak listy, mapy itp.
  2. Niezwykle niewygodna jest praca z odsyłaczami (np. Węzeł drzewa odwołujący się do swoich dzieci, podczas gdy dzieci odwołują się do swoich rodziców), co sprawia, że ​​w ogóle nie używam odsyłaczy, co ponownie sprawia, że ​​moje struktury danych i kod są bardziej funkcjonalne.
  3. Dziedziczenie przestaje mieć sens i zamiast tego zaczynam używać kompozycji.
  4. Całe podstawowe idee OOP, takie jak enkapsulacja, zaczynają się rozpadać, a moje obiekty zaczynają wyglądać jak funkcje.

W tym momencie praktycznie nie używam już nic z paradygmatu OOP i mogę po prostu przejść do czysto funkcjonalnego języka. Zatem moje pytanie: czy istnieje spójne podejście do dobrego niezmiennego projektu OOP, czy też zawsze jest tak, że kiedy wykorzystasz niezmienny pomysł do jego pełnego potencjału, zawsze kończysz programowanie w funkcjonalnym języku, który nie potrzebuje już niczego ze świata OOP? Czy istnieją jakieś dobre wytyczne, aby zdecydować, które klasy powinny być niezmienne, a które powinny być zmienne, aby zapewnić, że OOP się nie rozpadnie?

Dla wygody podam przykład. Zróbmy ChessBoardniezmienną kolekcję niezmiennych szachów (rozszerzenie klasy abstrakcyjnejPiece). Z punktu widzenia OOP, pionek jest odpowiedzialny za generowanie prawidłowych ruchów ze swojej pozycji na planszy. Ale aby wygenerować ruchy, element potrzebuje odniesienia do swojej planszy, podczas gdy plansza musi mieć odniesienie do swoich elementów. Cóż, istnieją pewne sztuczki, aby stworzyć te niezmienne powiązania w zależności od języka OOP, ale zarządzanie nimi jest uciążliwe, lepiej nie mieć elementu, który mógłby odwoływać się do jego tablicy. Ale wtedy element nie może wygenerować ruchów, ponieważ nie zna stanu planszy. Następnie kawałek staje się po prostu strukturą danych zawierającą typ elementu i jego pozycję. Następnie można użyć funkcji polimorficznej do generowania ruchów dla wszystkich rodzajów elementów. Jest to doskonale osiągalne w programowaniu funkcjonalnym, ale prawie niemożliwe w OOP bez sprawdzania typu środowiska wykonawczego i innych złych praktyk OOP ...


3
Podoba mi się podstawowe pytanie, ale mam trudności ze szczegółami. Np. Dlaczego dziedziczenie przestaje mieć sens, gdy maksymalizujesz niezmienność?
Martin Ba

1
Twoje obiekty nie mają metod?
Przestań krzywdzić Monikę


4
„Z punktu widzenia OOP element odpowiada za generowanie prawidłowych ruchów ze swojej pozycji na planszy.” - zdecydowanie nie, zaprojektowanie takiego kawałka najprawdopodobniej zaszkodzi SRP.
Doc Brown

1
@lishaak Ty „usiłujesz wyjaśnić problem dziedziczeniem w prostych słowach”, ponieważ nie jest to problem; problemem jest zła konstrukcja. Dziedziczenie jest samą esencją OOP, jeśli jest to konieczne, warunkiem koniecznym. Wiele tak zwanych „problemów z dziedziczeniem” to tak naprawdę problemy z językami nieposiadającymi jawnej składni zastępowania i są one znacznie mniej problematyczne w lepiej zaprojektowanych językach. Bez wirtualnych metod i polimorfizmu nie masz OOP; masz programowanie proceduralne ze śmieszną składnią obiektu. Nic więc dziwnego, że nie widzisz zalet OO, jeśli tego unikniesz!
Mason Wheeler

Odpowiedzi:


24

Czy naprawdę możemy zastosować niezmienność w OOP bez utraty wszystkich kluczowych funkcji OOP?

Nie rozumiem dlaczego nie. Robiłem to od lat, zanim Java 8 i tak stała się funkcjonalna. Słyszałeś kiedyś o Strunach? Ładne i niezmienne od samego początku.

  1. Zaczynam potrzebować trwałych (w sensie funkcjonalnym) struktur danych, takich jak listy, mapy itp.

Też cały czas ich potrzebowałem. Unieważnianie moich iteratorów, ponieważ zmutowałeś kolekcję podczas czytania, jest to po prostu niegrzeczne.

  1. Niezwykle niewygodna jest praca z odsyłaczami (np. Węzeł drzewa odwołujący się do swoich dzieci, podczas gdy dzieci odwołują się do swoich rodziców), co sprawia, że ​​w ogóle nie używam odsyłaczy, co ponownie sprawia, że ​​moje struktury danych i kod są bardziej funkcjonalne.

Okrągłe odniesienia to szczególny rodzaj piekła. Niezmienność cię przed tym nie uratuje.

  1. Dziedziczenie przestaje mieć sens i zamiast tego zaczynam używać kompozycji.

Cóż, jestem z tobą, ale nie widzę, co to ma wspólnego z niezmiennością. Powodem, dla którego lubię kompozycję, nie jest to, że uwielbiam dynamiczny wzorzec strategii, to dlatego, że pozwala mi zmieniać poziom abstrakcji.

  1. Całe podstawowe idee OOP, takie jak enkapsulacja, zaczynają się rozpadać, a moje obiekty zaczynają wyglądać jak funkcje.

Drżę, by pomyśleć, jaki jest twój pomysł na „enkapsulację OOP”. Jeśli dotyczy to programów pobierających i ustawiających, przestań nazywać to enkapsulacją, ponieważ tak nie jest. Nigdy tak nie było. To ręczne programowanie zorientowane na aspekt. Szansa na sprawdzenie i miejsce na punkt przerwania jest dobra, ale nie jest hermetyzacją. Kapsułkowanie zachowuje moje prawo do niewiedzy i dbania o to, co dzieje się w środku.

Twoje obiekty powinny wyglądać jak funkcje. To mnóstwo funkcji. Są to zestawy funkcji, które poruszają się razem i wspólnie się na nowo definiują.

W tej chwili modne jest programowanie funkcjonalne, a ludzie obawiają się pewnych nieporozumień na temat OOP. Nie pozwól, aby to pomieszało cię w przekonaniu, że to koniec OOP. Funkcjonalne i OOP mogą żyć całkiem nieźle.

  • Programowanie funkcjonalne jest formalnie związane z zadaniami.

  • OOP formalnie określa wskaźniki funkcji.

Naprawdę to. Dykstra powiedział nam, że gotojest szkodliwy, więc oficjalnie o tym poprosiliśmy i stworzyliśmy programowanie strukturalne. Właśnie tak, te dwa paradygmaty dotyczą znalezienia sposobów na załatwienie sprawy, unikając pułapek wynikających z robienia tych kłopotliwych rzeczy od niechcenia.

Pokażę ci coś:

f n (x)

To jest funkcja. To właściwie kontinuum funkcji:

f 1 (x)
f 2 (x)
...
f n (x)

Zgadnij, jak to wyrażamy w językach OOP?

n.f(x)

To niewiele ndecyduje o tym, która implementacja fjest używana ORAZ decyduje, jakie są niektóre ze stałych używanych w tej funkcji (co szczerze mówiąc to to samo). Na przykład:

f 1 (x) = x + 1
f 2 (x) = x + 2

To jest to samo, co zapewniają zamknięcia. Tam, gdzie zamknięcia odnoszą się do ich zakresu, metody obiektowe odnoszą się do stanu ich instancji. Obiekty mogą zrobić zamknięcia o jeden lepiej. Zamknięcie to pojedyncza funkcja zwrócona z innej funkcji. Konstruktor zwraca odwołanie do całego zestawu funkcji:

g 1 (x) = x 2 + 1
g 2 (x) = x 2 + 2

Zgadłeś:

n.g(x)

f i g to funkcje, które zmieniają się razem i poruszają razem. Więc wkładamy je do tej samej torby. Tak naprawdę jest przedmiotem. Trzymanie nstałej (niezmiennej) oznacza po prostu, że łatwiej jest przewidzieć, co zrobią, gdy do nich zadzwonisz.

To tylko struktura. Sposób, w jaki myślę o OOP, to kilka małych rzeczy, które mówią do innych małych rzeczy. Mam nadzieję, że do małej wybranej grupy drobiazgów. Kiedy koduję, wyobrażam sobie siebie jako obiekt. Patrzę na rzeczy z punktu widzenia obiektu. Staram się być leniwy, żeby nie przerabiać obiektu. Przyjmuję proste wiadomości, trochę nad nimi pracuję i wysyłam proste wiadomości tylko do moich najlepszych przyjaciół. Kiedy skończę z tym obiektem, wskakuję na inny i patrzę na rzeczy z jego perspektywy.

Karty odpowiedzialności klasowej jako pierwsze nauczyły mnie myśleć w ten sposób. Człowieku, byłem wtedy zdezorientowany, ale cholera, jeśli nadal nie są aktualne.

Miejmy ChessBoard jako niezmienny zbiór niezmiennych pionków szachowych (rozszerzenie klasy abstrakcyjnej). Z punktu widzenia OOP, pionek jest odpowiedzialny za generowanie prawidłowych ruchów ze swojej pozycji na planszy. Ale aby wygenerować ruchy, element potrzebuje odniesienia do swojej planszy, podczas gdy plansza musi mieć odniesienie do swoich elementów.

Arg! Ponownie z niepotrzebnymi okólnikami.

Co powiesz na: A ChessBoardDataStructurezamienia sznurki xy w odniesienia do elementów. Te elementy mają metodę, która bierze x, y i konkretną, ChessBoardDataStructurei zamienia ją w kolekcję brandów, na której pojawiają się nowe ChessBoardDataStructure. Następnie wpycha to w coś, co może wybrać najlepszy ruch. Teraz ChessBoardDataStructuremogą być niezmienne, podobnie jak kawałki. W ten sposób masz tylko jeden biały pionek w pamięci. Istnieje tylko kilka odniesień do tego w odpowiednich lokalizacjach XY. Obiektowy, funkcjonalny i niezmienny.

Zaraz, czy nie rozmawialiśmy już o szachach?


6
Każdy powinien to przeczytać. A następnie przeczytaj artykuł „ Understanding Data Abstraction”, ponownie autor: William R. Cook . A potem przeczytaj to jeszcze raz. A potem przeczytaj Propozycję Cooka dotyczącą uproszczonych, nowoczesnych definicji „Object” i „Object Oriented” . Wrzuć Alana Kaya „ Ideą jest„ przesyłanie wiadomości ” ”, jego definicja OO
Jörg W Mittag


1
@Euphoric: Myślę, że jest to dość standardowe sformułowanie, które pasuje do standardowych definicji „programowania funkcjonalnego”, „programowania imperatywnego”, „programowania logicznego” itp. W przeciwnym razie C jest językiem funkcjonalnym, ponieważ można w nim zakodować FP, język logiczny, ponieważ można w nim zakodować programowanie logiczne, język dynamiczny, ponieważ można w nim kodować dynamiczne pisanie, język aktora, ponieważ można w nim zakodować system aktorski i tak dalej. A Haskell jest największym na świecie językiem imperatywnym, ponieważ faktycznie można nazywać skutki uboczne, przechowywać je w zmiennych, przekazywać jako ...
Jörg W Mittag

1
Myślę, że możemy użyć podobnych pomysłów jak w genialnym artykule Matthiasa Felleisena na temat ekspresji języka. Przeniesienie programu OO, powiedzmy, z Java do C♯ można wykonać tylko przy użyciu lokalnych przekształceń, ale przeniesienie go do C wymaga globalnej restrukturyzacji (w zasadzie trzeba wprowadzić mechanizm wysyłania komunikatów i przekierować wszystkie wywołania funkcji za jego pośrednictwem), dlatego Java i C♯ mogą wyrażać OO, a C może „tylko” kodować.
Jörg W Mittag

1
@lishaak Ponieważ określasz to dla różnych rzeczy. Utrzymuje to jeden poziom abstrakcji i zapobiega duplikowaniu przechowywania informacji o położeniu, które w przeciwnym razie mogłyby stać się niespójne. Jeśli po prostu nie podoba ci się dodatkowe pisanie, umieść je w metodzie, aby wpisać je tylko raz. Kawałek nie musi pamiętać, gdzie jest, jeśli powiesz mu, gdzie jest. Teraz elementy przechowują tylko takie informacje, jak kolor, ruchy i obraz / skrót, które są zawsze prawdziwe i niezmienne.
candied_orange

2

Moim zdaniem najbardziej użytecznymi pojęciami wprowadzonymi do głównego nurtu przez OOP są:

  • Właściwa modularyzacja.
  • Hermetyzacja danych.
  • Wyraźne oddzielenie interfejsów prywatnych od publicznych.
  • Jasne mechanizmy rozszerzalności kodu.

Wszystkie te korzyści można również zrealizować bez tradycyjnych szczegółów implementacji, takich jak dziedziczenie, a nawet klasy. Oryginalny pomysł Alana Kay na „system obiektowy” wykorzystywał „wiadomości” zamiast „metod” i był bliższy Erlangowi niż np. C ++. Spójrz na Go, która eliminuje wiele tradycyjnych szczegółów implementacji OOP, ale nadal wydaje się być dość zorientowana obiektowo.

Jeśli używasz niezmiennych obiektów, nadal możesz korzystać z większości tradycyjnych dodatków OOP: interfejsów, dynamicznej wysyłki, enkapsulacji. Nie potrzebujesz seterów i często nie potrzebujesz nawet getterów dla prostszych obiektów. Możesz także czerpać korzyści z niezmienności: zawsze masz pewność, że w międzyczasie obiekt się nie zmienił, nie będzie kopiowania obronnego, żadnych wyścigów danych, a metody są czyste.

Zobacz, jak Scala próbuje połączyć niezmienność i podejście FP z OOP. Trzeba przyznać, że nie jest to najprostszy i najbardziej elegancki język. Jednak jest praktycznie praktycznie udany. Zobacz także Kotlin, który oferuje wiele narzędzi i podejść do podobnego miksu.

Zwykłym problemem związanym z wypróbowaniem innego podejścia niż to, o którym twórcy języka mieli na myśli N lata temu, jest „niedopasowanie impedancji” ze standardową biblioteką. OTOH zarówno ekosystemy Java, jak i .NET mają obecnie rozsądną obsługę standardowej biblioteki dla niezmiennych struktur danych; oczywiście istnieją również biblioteki innych firm.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.