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 Person
z elementem father
odwołującym się do innego Person
obiektu. 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.
źródło
Odpowiedzi:
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ć
size
metodę wIList
instancji, ale wywołanie może zostać rozstrzygnięte na implementację wLinkedList
klasie). 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ść.
źródło
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.
źródło
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
for
pętle (ponieważ musiałbyś zmutować zmienną pętli) na rzecz rekurencji, a teraz zasadniczo programujesz w sposób funkcjonalny.źródło
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:
Żadne odniesienie do obiektu nigdy nie będzie narażone na cokolwiek, co mogłoby zmienić stan w nim zawarty.
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.
źródło
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
StringBuilder
klasy zamiaststring
klasy.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.
źródło
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.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)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.
źródło
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.
W stylu przekazywania wiadomości mówisz obiektowi, aby zapisał nowe dane, zamiast pytać, jakie nowe dane powinieneś zapisać w nowej zmiennej.
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.
Nie można jednak łączyć stylu przekazywania wiadomości z niezmiennymi obiektami.
źródło