Pełna niezmienność i programowanie obiektowe

43

W większości języków OOP obiekty są ogólnie modyfikowalne z ograniczonym zestawem wyjątków (takich jak np. Krotki i ciągi w pythonie). W większości języków funkcjonalnych dane są niezmienne.

Zarówno zmienne, jak i niezmienne obiekty wnoszą własną listę zalet i wad.

Istnieją języki, które próbują łączyć oba pojęcia, takie jak np. Scala, w którym (jawnie zadeklarowano) zmienne i niezmienne dane (proszę mnie poprawić, jeśli się mylę, moja wiedza na temat scala jest więcej niż ograniczona).

Moje pytanie brzmi: czy całkowita (sic!) Niezmienność - czyli żaden obiekt nie może mutować po utworzeniu - ma jakiś sens w kontekście OOP?

Czy istnieją projekty lub wdrożenia takiego modelu?

Zasadniczo, czy (całkowita) niezmienność i przeciwieństwa OOP są ortogonalne?

Motywacja: w OOP zwykle operujesz na danych, zmieniając (mutując) podstawowe informacje, zachowując odniesienia między tymi obiektami. Np. Obiekt klasy Personz elementem fatherodwołującym się do innego Personobiektu. Jeśli zmienisz imię ojca, będzie ono natychmiast widoczne dla obiektu potomnego bez potrzeby aktualizacji. Będąc niezmiennym, musisz budować nowe przedmioty zarówno dla ojca, jak i dziecka. Ale miałbyś o wiele mniej kerfuffle ze współdzielonymi obiektami, wielowątkowością, GIL itp.

Hyperboreus
źródło
2
Niezmienność można symulować w języku OOP, ujawniając punkty dostępu do obiektu jako metody lub właściwości tylko do odczytu, które nie mutują danych. Niezmienność działa tak samo w językach OOP, jak w każdym języku funkcjonalnym, z tym wyjątkiem, że brakuje niektórych funkcji języka funkcjonalnego.
Robert Harvey
4
Zmienność nie jest własnością języków OOP, takich jak C # i Java, ani nie jest niezmienność. Określasz zmienność lub niezmienność przez sposób pisania klasy.
Robert Harvey
9
Zakłada się, że zmienność jest podstawową cechą orientacji obiektowej. To nie jest Zmienność jest po prostu własnością obiektów lub wartości. Orientacja obiektowa obejmuje szereg nieodłącznych pojęć (enkapsulacja, polimorfizm, dziedziczenie itp.), Które mają niewiele lub nie mają nic wspólnego z mutacją, a Ty nadal czerpałbyś korzyści z tych cech. nawet jeśli wszystko uczynisz niezmiennym.
Robert Harvey
2
@MichaelT Pytanie nie dotyczy modyfikowania określonych rzeczy, lecz uczynienia wszystkich rzeczy niezmiennymi.

Odpowiedzi:

43

OOP i niezmienność są do siebie prawie całkowicie ortogonalne. Jednak imperatywne programowanie i niezmienność nie są.

OOP można podsumować za pomocą dwóch podstawowych funkcji:

  • Hermetyzacja : Nie będę uzyskiwać bezpośredniego dostępu do zawartości obiektów, ale komunikuję się za pośrednictwem określonego interfejsu („metod”) z tym obiektem. Ten interfejs może ukrywać przede mną dane wewnętrzne. Technicznie jest to specyficzne dla programowania modułowego, a nie OOP. Dostęp do danych przez zdefiniowany interfejs jest w przybliżeniu równoważny abstrakcyjnemu typowi danych.

  • Dynamiczna wysyłka : Kiedy wywołam metodę na obiekcie, wykonana metoda zostanie rozwiązana w czasie wykonywania. (Np. W OOP opartym na klasach mógłbym wywołać sizemetodę w IListinstancji, ale wywołanie może zostać rozstrzygnięte na implementację w LinkedListklasie). Dynamiczna wysyłka jest jednym ze sposobów na zachowanie polimorficzne.

Hermetyzacja ma mniej sensu bez zmienności (nie ma stanu wewnętrznego, który mógłby zostać uszkodzony przez zewnętrzne wtrącanie się), ale nadal ułatwia abstrakcje, nawet gdy wszystko jest niezmienne.

Program imperatywny składa się z instrukcji wykonywanych sekwencyjnie. Instrukcja ma skutki uboczne, takie jak zmiana stanu programu. Przy niezmienności stanu nie można zmienić (oczywiście można stworzyć nowy stan). Dlatego programowanie imperatywne jest zasadniczo niezgodne z niezmiennością.

Zdarza się, że OOP historycznie zawsze był związany z programowaniem imperatywnym (Simula jest oparty na Algolu), a wszystkie główne języki OOP mają imperatywne korzenie (C ++, Java, C #,… wszystkie są zakorzenione w C). Nie oznacza to, że sam OOP byłby konieczny lub zmienny, to po prostu oznacza, że ​​implementacja OOP przez te języki pozwala na zmienność.

amon
źródło
2
Dziękuję bardzo, szczególnie za definicję dwóch podstawowych funkcji.
Hyperboreus,
Dynamiczna wysyłka nie jest podstawową funkcją OOP. Tak naprawdę tak naprawdę nie jest Encapsulation (jak już przyznałeś).
Stop Harming Monica
5
@OrangeDog Tak, nie ma powszechnie akceptowanej definicji OOP, ale potrzebowałem definicji do pracy. Wybrałem więc coś, co jest tak blisko prawdy, jak to tylko możliwe, bez napisania o tym całej rozprawy. Uważam jednak dynamiczną wysyłkę za jedną główną cechę odróżniającą OOP od innych paradygmatów. Coś, co wygląda jak OOP, ale faktycznie wszystkie wywołania są rozstrzygane statycznie, jest tak naprawdę tylko programowaniem modułowym z polimorfizmem ad-hoc. Obiekty są parą metod i danych i jako takie są równoważne zamknięciom.
amon
2
„Obiekty są parą metod i danych”. Wszystko czego potrzebujesz to coś, co nie ma nic wspólnego z niezmiennością.
Stop Harming Monica
3
@CodeYogi Ukrywanie danych jest najczęstszym rodzajem enkapsulacji. Jednak sposób, w jaki dane są przechowywane wewnętrznie przez obiekt, nie jest jedynym szczegółem implementacji, który należy ukryć. Równie ważne jest, aby ukryć sposób implementacji interfejsu publicznego, np. Czy korzystam z jakichkolwiek metod pomocniczych. Ogólnie rzecz biorąc, takie metody pomocnicze powinny być również prywatne. Podsumowując: enkapsulacja jest zasadą, podczas gdy ukrywanie danych jest techniką enkapsulacji.
amon
25

Zauważ, że wśród programistów zorientowanych obiektowo jest kultura, w której ludzie zakładają, że robisz OOP, że większość twoich obiektów będzie podlegać modyfikacjom, ale jest to odrębny problem od tego, czy OOP wymaga zmienności. Ponadto kultura ta wydaje się powoli zmieniać w kierunku większej niezmienności z powodu narażenia ludzi na programowanie funkcjonalne.

Scala jest naprawdę dobrą ilustracją, że zmienność nie jest wymagana do orientacji obiektowej. Chociaż Scala obsługuje zmienność, jego stosowanie jest odradzane. Idiomatic Scala jest bardzo zorientowany obiektowo, a także prawie całkowicie niezmienny. W większości pozwala na zmienność kompatybilności z Javą, a ponieważ w pewnych okolicznościach niezmienne obiekty są nieefektywne lub skomplikowane w pracy.

Porównaj na przykład listę Scala i listę Java . Niezmienna lista Scali zawiera wszystkie te same metody obiektowe, co lista mutable Javy. Co więcej, ponieważ Java używa funkcji statycznych do operacji takich jak sort , a Scala dodaje metody typu funkcjonalnego, takie jak map. Wszystkie cechy OOP - enkapsulacja, dziedziczenie i polimorfizm - są dostępne w formie znanej programistom obiektowym i odpowiednio używane.

Jedyną różnicą, którą zobaczysz, jest to, że po zmianie listy otrzymujesz nowy obiekt. To często wymaga użycia innych wzorów projektowych niż w przypadku obiektów zmiennych, ale nie wymaga całkowitego porzucenia OOP.

Karl Bielefeldt
źródło
17

Niezmienność można symulować w języku OOP, ujawniając punkty dostępu do obiektów jako metody lub właściwości tylko do odczytu, które nie mutują danych. Niezmienność działa tak samo w językach OOP, jak w każdym języku funkcjonalnym, z tym wyjątkiem, że brakuje niektórych funkcji języka funkcjonalnego.

Zakłada się, że zmienność jest podstawową cechą orientacji obiektowej. Ale zmienność jest po prostu własnością przedmiotów lub wartości. Orientacja obiektowa obejmuje szereg nieodłącznych pojęć (enkapsulacja, polimorfizm, dziedziczenie itp.), Które mają niewiele lub nie mają nic wspólnego z mutacją, a ty nadal czerpałbyś korzyści z tych cech, nawet gdybyś uczynił wszystko niezmiennym.

Nie wszystkie języki funkcjonalne również wymagają niezmienności. Clojure ma specjalną adnotację, która umożliwia modyfikowanie typów, a większość „praktycznych” języków funkcjonalnych umożliwia określenie typów modyfikowalnych.

Lepszym pytaniem może być: „Czy całkowita niezmienność ma sens w programowaniu imperatywnym ?” Powiedziałbym, że oczywistą odpowiedzią na to pytanie jest „nie”. Aby osiągnąć całkowitą niezmienność w programowaniu imperatywnym, musiałbyś zrezygnować z rzeczy takich jak forpętle (ponieważ musiałbyś zmutować zmienną pętli) na rzecz rekurencji, a teraz zasadniczo programujesz w sposób funkcjonalny.

Robert Harvey
źródło
Dziękuję Ci. Czy mógłbyś rozwinąć nieco swój ostatni akapit („oczywiste” może być nieco subiektywne).
Hyperboreus,
Już zrobiłem ....
Robert Harvey
1
@Hyperboreus Istnieje wiele sposobów na osiągnięcie polimorfizmu. Subtyping z dynamiczną wysyłką, statyczny polimorfizm ad-hoc (aka. Przeciążenie funkcji) i parametryczny polimorfizm (aka generics) są najczęstszymi sposobami na to, a wszystkie sposoby mają swoje mocne i słabe strony. Współczesne języki OOP łączą wszystkie te trzy sposoby, podczas gdy Haskell opiera się przede wszystkim na polimorfizmie parametrycznym i polimorfizmie ad hoc.
amon
3
@RobertHarvey Mówisz, że potrzebujesz zmienności, ponieważ musisz zapętlić (w przeciwnym razie musisz użyć rekurencji). Zanim zacząłem używać Haskell dwa lata temu, pomyślałem, że potrzebuję również zmiennych zmiennych. Mówię tylko, że istnieją inne sposoby „zapętlenia” (mapowania, składania, filtrowania itp.). Po wyłączeniu zapętlania tabeli, po co mielibyście mieć zmienne zmienne?
cimmanon
1
@RobertHarvey Ale właśnie o to chodzi w językach programowania: co jest narażone, a nie to, co dzieje się pod maską. Ten ostatni to odpowiedzialność kompilatora lub interpretera, a nie twórcy aplikacji. W przeciwnym razie wróć do asemblera.
Hyperboreus,
5

Często przydatne jest kategoryzowanie obiektów jako enkapsulujące wartości lub byty, z tym wyjątkiem, że jeśli coś jest wartością, kod, który zawiera odniesienie do niego, nigdy nie powinien widzieć zmiany stanu w żaden sposób, którego sam kod nie zainicjował. Natomiast kod, który zawiera odniesienie do jednostki, może oczekiwać, że zmieni się w sposób niezależny od posiadacza referencji.

Chociaż możliwe jest użycie enkapsulacji wartości przy użyciu obiektów zmiennych lub niezmiennych, obiekt może zachowywać się jak wartość tylko wtedy, gdy spełniony jest co najmniej jeden z następujących warunków:

  1. Żadne odniesienie do obiektu nigdy nie będzie narażone na cokolwiek, co mogłoby zmienić stan w nim zawarty.

  2. Posiadacz co najmniej jednego z odniesień do obiektu zna wszystkie zastosowania, do których mogłoby dojść jakiekolwiek istniejące odniesienie.

Ponieważ wszystkie wystąpienia typów niezmiennych automatycznie spełniają pierwsze wymaganie, używanie ich jako wartości jest łatwe. Z drugiej strony, zapewnienie, że którykolwiek z wymogów jest spełniony podczas korzystania ze zmiennych typów, jest znacznie trudniejsze. Podczas gdy odniesienia do typów niezmiennych można swobodnie przekazywać jako sposób enkapsulacji stanu w nich zawartego, przekazywanie stanu przechowywanego w typach zmiennych wymaga albo zbudowania niezmiennych obiektów owijających, albo skopiowania stanu enkapsulowanego przez obiekty prywatne do innych obiektów, które są dostarczone lub skonstruowane dla odbiorcy danych.

Niezmienne typy działają bardzo dobrze do przekazywania wartości i często są przynajmniej w pewnym stopniu przydatne do manipulowania nimi. Nie są jednak tak dobrzy w obsłudze podmiotów. Najbliższą rzeczą, jaką można mieć do bytu w systemie z typami czysto niezmiennymi, jest funkcja, która, biorąc pod uwagę stan systemu, zgłosi te atrybuty jego części lub wytworzy nową instancję stanu systemu, która jest jak pod warunkiem, z wyjątkiem niektórych jego szczególnych części, które będą się różnić w pewien wybierany sposób. Ponadto, jeśli celem bytu jest połączenie jakiegoś kodu z czymś, co istnieje w świecie rzeczywistym, może być niemożliwe uniknięcie ujawnienia stanu zmienności.

Na przykład, jeśli ktoś odbierze jakieś dane przez połączenie TCP, może wytworzyć nowy obiekt „stan świata”, który zawiera te dane w swoim buforze bez wpływu na jakiekolwiek odniesienia do starego „stanu świata”, ale stare kopie stan świata, który nie obejmuje ostatniej partii danych, będzie wadliwy i nie powinien być używany, ponieważ nie będą już pasować do stanu gniazda TCP w świecie rzeczywistym.

supercat
źródło
4

W języku c # niektóre typy są niezmienne jak ciąg.

Wydaje się to sugerować, że wybór został mocno rozważony.

Na pewno naprawdę wymaga to użycia niezmiennych typów, jeśli trzeba zmodyfikować ten typ setki tysięcy razy. Dlatego w tych przypadkach sugeruje się użycie StringBuilderklasy zamiast stringklasy.

Zrobiłem eksperyment z profilerem i użycie niezmiennego typu wymaga naprawdę więcej procesora i pamięci RAM.

Jest to również intuicyjne, jeśli weźmiesz pod uwagę, że aby zmodyfikować tylko jedną literę w ciągu 4000 znaków, musisz skopiować każdy znak w innym obszarze pamięci RAM.

Revious
źródło
6
Częste modyfikowanie niezmiennych danych nie musi być katastrofalnie powolne, jak w przypadku wielokrotnego stringłączenia. Dla praktycznie wszystkich rodzajów danych / przypadków użycia można (często już) opracować wydajną trwałą strukturę. Większość z nich ma w przybliżeniu jednakową wydajność, nawet jeśli stałe czynniki są czasem gorsze.
@delnan Myślę również, że ostatni akapit odpowiedzi dotyczy bardziej szczegółów implementacyjnych niż (im) zmienności.
Hyperboreus,
@Hyperboreus: czy uważasz, że powinienem usunąć tę część? Ale jak łańcuch może się zmienić, jeśli jest niezmienny? Mam na myśli ... moim zdaniem, ale na pewno mogę się mylić, może to być główny powód, dla którego obiekty nie są niezmienne.
Revious
1
@Revious W żadnym wypadku. Zostaw to, więc spowoduje dyskusję i ciekawsze opinie i punkty widzenia.
Hyperboreus,
1
@Revious Tak, czytanie byłoby wolniejsze, ale nie tak powolne jak zmiana string(tradycyjnej reprezentacji). „Ciąg” (w reprezentacji, o której mówię) po 1000 modyfikacjach byłby jak świeżo utworzony ciąg (zawartość modulo); żadna użyteczna lub powszechnie stosowana trwała struktura danych nie pogarsza jakości po operacjach X. Fragmentacja pamięci nie jest poważnym problemem (miałbyś wiele alokacji, tak, ale fragmentacja nie jest problemem w nowoczesnych śmieciarzach)
0

Całkowita niezmienność wszystkiego nie ma większego sensu w OOP ani w większości innych paradygmatów z tego powodu, z jednego bardzo ważnego powodu:

Każdy przydatny program ma skutki uboczne.

Program, który niczego nie zmienia, jest bezwartościowy. Równie dobrze możesz go nawet nie uruchomić, ponieważ efekt będzie identyczny.

Nawet jeśli uważasz, że niczego nie zmieniasz i po prostu podsumowujesz listę liczb, które w jakiś sposób otrzymałeś, zastanów się, że musisz coś zrobić z wynikiem - czy wydrukujesz to na standardowym wyjściu, zapiszesz w pliku, lub gdziekolwiek. A to wiąże się z mutacją bufora i zmianą stanu systemu.

Ograniczenie zmienności do części, które należy zmienić, może mieć sens . Ale jeśli absolutnie nic nie musi się zmienić, to nie robisz nic wartego zrobienia.

cHao
źródło
4
Nie rozumiem, jak twoja odpowiedź odnosi się do pytania, ponieważ nie zajmowałem się czysto funkcjonalnymi językami. Weźmy na przykład erlang: niezmienne dane, brak destrukcyjnego przypisania, brak mgiełki na temat skutków ubocznych. Również masz stan w języku funkcjonalnym, tyle że stan „przepływa” przez funkcje, w przeciwieństwie do funkcji działających na tym stanie. Stan zmienia się, ale nie mutuje w miejscu, ale stan przyszły zastępuje obecny stan. Niezmienność nie polega na tym, czy bufor pamięci zostanie zmieniony, czy nie, chodzi o to, czy mutacje te są widoczne z zewnątrz.
Hyperboreus,
A przyszły stan zastępuje obecny, jak dokładnie? W programie OO ten stan jest gdzieś własnością obiektu. Zastąpienie stanu wymaga zmiany obiektu (lub zastąpienia go innym, co wymaga zmiany obiektów, które się do niego odnoszą (lub zastąpienia go innym, co ... eh. Masz rację). Możesz wymyślić jakiś monadyczny hack, w którym każda akcja kończy się tworzeniem zupełnie nowej aplikacji ... ale nawet wtedy gdzieś program musi gdzieś zostać zarejestrowany.
cHao
7
-1. To jest niepoprawne. Mylisz efekty uboczne z mutacją i chociaż często są one traktowane tak samo przez funkcjonalne języki, są różne. Każdy przydatny program ma skutki uboczne; nie każdy przydatny program ma mutację.
Michael Shaw
@Michael: Jeśli chodzi o OOP, mutacja i skutki uboczne są tak powiązane, że nie można ich realistycznie oddzielić. Jeśli nie masz mutacji, nie możesz mieć skutków ubocznych bez ogromnej ilości hakerów.
cHao
-2

Myślę, że to zależy od tego, czy twoja definicja OOP polega na tym, że używa stylu przekazywania wiadomości.

Czyste funkcje nie muszą niczego mutować, ponieważ zwracają wartości, które można zapisać w nowych zmiennych.

var brandNewVariable = pureFunction(foo);

W stylu przekazywania wiadomości mówisz obiektowi, aby zapisał nowe dane, zamiast pytać, jakie nowe dane powinieneś zapisać w nowej zmiennej.

sameOldObject.changeMe(foo);

Możliwe jest posiadanie obiektów i nie mutowanie ich, czyniąc z tych metod czyste funkcje, które zdarzają się żyć wewnątrz obiektu zamiast na zewnątrz.

var brandNewVariable = nonMutatingObject.askMe(foo);

Nie można jednak łączyć stylu przekazywania wiadomości z niezmiennymi obiektami.

presley
źródło