Jak decyduje się, czy typ obiektu danych powinien być niezmienny?


23

Uwielbiam niezmienny „wzorzec” ze względu na jego mocne strony, aw przeszłości uważałem za korzystne projektowanie systemów z niezmiennymi typami danych (niektóre, większość lub nawet wszystkie). Często kiedy to robię, piszę mniej błędów, a debugowanie jest znacznie łatwiejsze.

Jednak moi rówieśnicy w ogóle unikają niezmienności. Nie są wcale niedoświadczeni (z dala od tego), ale piszą obiekty danych w klasyczny sposób - prywatni członkowie z getterem i setersem dla każdego członka. Wtedy zwykle ich konstruktorzy nie przyjmują argumentów, a może po prostu biorą argumenty dla wygody. Tak często tworzenie obiektu wygląda następująco:

Foo a = new Foo();
a.setProperty1("asdf");
a.setProperty2("bcde");

Może robią to wszędzie. Może nawet nie definiują konstruktora, który bierze te dwa ciągi, bez względu na to, jak ważne są. I może nie zmieniają później wartości tych ciągów i nigdy nie muszą tego robić. Oczywiście, jeśli te rzeczy są prawdziwe, obiekt byłby lepiej zaprojektowany jako niezmienny, prawda? (konstruktor przyjmuje dwie właściwości, w ogóle nie ma seterów).

Jak decydujesz, czy typ obiektu powinien zostać zaprojektowany jako niezmienny? Czy istnieje dobry zestaw kryteriów do oceny?

Obecnie zastanawiam się, czy zmienić kilka typów danych w moim projekcie na niezmienne, ale musiałbym uzasadnić to moim rówieśnikom, a dane w tych typach mogą (BARDZO rzadko) ulec zmianie - w którym momencie możesz oczywiście zmienić to niezmienny sposób (utwórz nowy, kopiując właściwości ze starego obiektu, z wyjątkiem tych, które chcesz zmienić). Ale nie jestem pewien, czy to tylko moja miłość do przejawiania się niezmienności, czy też istnieje rzeczywista potrzeba / korzyść z nich.


1
Program w Erlang i cały problem został rozwiązany, wszystko jest niezmienne
Zachary K

1
@ZacharyK Właściwie myślałem o czymś o programowaniu funkcjonalnym, ale powstrzymałem się z powodu mojego ograniczonego doświadczenia z funkcjonalnymi językami programowania.
Ricket

Odpowiedzi:


27

Wygląda na to, że zbliżasz się do niego wstecz. Należy domyślnie niezmienny. Zmodyfikuj obiekt tylko wtedy, gdy absolutnie musisz / po prostu nie możesz sprawić, aby działał jako obiekt niezmienny.


11

Podstawową zaletą niezmiennych obiektów jest zagwarantowanie bezpieczeństwa nici. W świecie, w którym wiele rdzeni i nici jest normą, ta korzyść stała się bardzo ważna.

Ale korzystanie ze zmiennych obiektów jest bardzo wygodne. Działają dobrze i dopóki nie modyfikujesz ich z oddzielnych wątków i dobrze rozumiesz, co robisz, są one dość niezawodne.


15
Zaletą niezmiennych obiektów jest to, że łatwiej je zrozumieć. W środowisku wielowątkowym jest to o wiele łatwiejsze; w prostych algorytmach jednowątkowych jest to jeszcze łatwiejsze. Np. Nigdy nie musisz dbać o to, czy stan jest spójny, czy obiekt przeszedł wszystkie mutacje potrzebne do użycia w określonym kontekście itp. Najlepsze obiekty zmienne pozwalają ci niewiele dbać o ich dokładny stan: np. Pamięci podręczne.
9000

-1, zgadzam się z @ 9000. Bezpieczeństwo wątków jest drugorzędne (rozważ obiekty, które pojawiają się w niezmiennym stanie, ale mają wewnętrzny stan zmienny z powodu np. Zapamiętywania). Również podniesienie wydajności zmienności jest przedwczesną optymalizacją i możliwe jest bronienie czegokolwiek, jeśli wymaga się, aby użytkownik „wiedział, co robi”. Gdybym wiedział, co robię przez cały czas, nigdy nie napisałbym programu z błędem.
Doval

4
@Doval: Nie ma co się z tym nie zgadzać. 9000 ma absolutną rację; niezmienne obiekty łatwiejsze do rozumu temat. To częściowo sprawia, że ​​są one tak przydatne do programowania współbieżnego. Z drugiej strony nie musisz się martwić o zmianę stanu. Przedwczesna optymalizacja nie ma tu znaczenia; użycie niezmiennych obiektów dotyczy projektowania, a nie wydajności, a zły wybór struktur danych z góry to przedwczesna pesymizacja.
Robert Harvey

@RobertHarvey Nie jestem pewien, co rozumiesz przez „zły wybór struktury danych z góry”. Istnieją niezmienne wersje większości zmiennych danych, które zapewniają podobną wydajność. Jeśli potrzebujesz listy i masz do wyboru listę niezmienną i zmienną, idź z tą niezmienną, dopóki nie upewnisz się, że jest to wąskie gardło w twojej aplikacji, a wersja zmienna będzie działać lepiej.
Doval

2
@Doval: Przeczytałem Okasaki. Te struktury danych nie są powszechnie używane, chyba że używasz języka w pełni obsługującego paradygmat funkcjonalny, takiego jak Haskell lub Lisp. I kwestionuję pogląd, że niezmienne struktury są domyślnym wyborem; zdecydowana większość biznesowych systemów komputerowych jest nadal projektowana wokół struktur zmiennych (tj. relacyjnych baz danych). Zaczynanie od niezmiennych struktur danych to dobry pomysł, ale nadal jest to bardzo kość słoniowa.
Robert Harvey

6

Istnieją dwa główne sposoby decydowania, czy obiekt jest niezmienny.

a) W oparciu o naturę obiektu

Łatwo jest uchwycić te sytuacje, ponieważ wiemy, że te obiekty nie zmienią się po zbudowaniu. Na przykład, jeśli masz RequestHistoryencję i z natury rzeczy encje nie zmieniają się po jej zbudowaniu. Obiekty te można od razu zaprojektować jako niezmienne klasy. Należy pamiętać, że obiekt żądania jest zmienny, ponieważ może zmieniać swój stan i do kogo jest przypisany itp., Ale historia żądań nie ulega zmianie. Na przykład w zeszłym tygodniu utworzono element historii, który przeniósł się ze stanu przesłanego do przypisanego ORAZ ten podmiot historii nigdy się nie zmieni. Jest to więc klasyczna niezmienna obudowa.

b) W oparciu o wybór projektu czynniki zewnętrzne

Jest to podobne do przykładu java.lang.String. Ciągi mogą się zmieniać w czasie, ale z założenia sprawiły, że są niezmienne ze względu na buforowanie / pulę ciągów / czynniki współbieżności. Podobnie buforowanie / współbieżność itp. Może odgrywać dobrą rolę w uczynieniu obiektu odpornym na ataki, jeśli buforowanie / współbieżność i związana z nim wydajność są niezbędne w aplikacji. Ale decyzję tę należy podjąć bardzo ostrożnie po uprzednim przeanalizowaniu wszystkich skutków.

Główną zaletą niezmiennych obiektów jest to, że nie są poddawane wzorom chwastowania. Obiekt nie wykryje żadnych zmian w czasie życia, a to bardzo ułatwia kodowanie i konserwację.


4

Obecnie zastanawiam się, czy zmienić kilka typów danych w moim projekcie na niezmienne, ale musiałbym uzasadnić to moim rówieśnikom, a dane w tych typach mogą (BARDZO rzadko) ulec zmianie - w którym momencie możesz oczywiście zmienić to niezmienny sposób (utwórz nowy, kopiując właściwości ze starego obiektu, z wyjątkiem tych, które chcesz zmienić).

Minimalizacja stanu programu jest bardzo korzystna.

Zapytaj ich, czy chcą użyć zmiennego typu wartości w jednej z twoich klas do tymczasowego przechowywania z klasy klienta.

Jeśli powiedzą tak, zapytaj dlaczego? Stan zmienny nie należy do takiego scenariusza. Zmuszenie ich do utworzenia stanu, w którym faktycznie należy, i uczynienie stanu typów danych tak wyraźnymi, jak to możliwe, to doskonałe powody.


4

Odpowiedź jest nieco zależna od języka. Twój kod wygląda jak Java, gdzie ten problem jest tak trudny, jak to możliwe. W Javie obiekty mogą być przekazywane tylko przez odniesienie, a klon jest całkowicie uszkodzony.

Nie ma prostej odpowiedzi, ale na pewno chcesz uczynić obiekty o małej wartości niezmiennymi. Java poprawnie spowodowała, że ​​ciągi są niezmienne, ale niepoprawnie zmieniono datę i kalendarz.

Zdecydowanie więc unieważnij obiekty o małej wartości i zaimplementuj konstruktor kopii. Zapomnij o Cloneable, jest tak źle zaprojektowany, że jest bezużyteczny.

W przypadku obiektów o większej wartości, jeśli niewygodne jest uczynienie ich niezmiennymi, należy je łatwo skopiować.


Brzmi jak wybór między stosem a stosem podczas pisania w C lub C ++ :)
Ricket

@Ricket: nie tyle IMO. Stos / stos zależy od czasu życia obiektu. W C ++ bardzo często występują zmienne obiekty na stosie.
kevin cline

1

I może nie zmieniają później wartości tych ciągów i nigdy nie muszą tego robić. Oczywiście, jeśli te rzeczy są prawdziwe, obiekt byłby lepiej zaprojektowany jako niezmienny, prawda?

Wręcz przeciwnie intuicyjnie, to, że nigdy nie trzeba później zmieniać łańcuchów, jest całkiem dobrym argumentem, że nie ma znaczenia, czy obiekty są niezmienne, czy nie. Programista traktuje je już jako niezmienne, niezależnie od tego, czy kompilator to wymusza, czy nie.

Niezmienność zwykle nie boli, ale też nie zawsze pomaga. Najłatwiejszym sposobem stwierdzenia, czy twój obiekt może skorzystać z niezmienności, jest to, że kiedykolwiek będziesz musiał wykonać kopię obiektu lub nabyć muteks przed jego zmianą . Jeśli to się nigdy nie zmienia, to niezmienność tak naprawdę niczego nie kupuje, a czasem komplikuje sprawę.

Masz rację, jeśli chodzi o ryzyko konstruowania obiektu w nieprawidłowym stanie, ale to naprawdę kwestia odrębna od niezmienności. Obiekt może być zarówno zmienny, jak i zawsze w poprawnym stanie po zakończeniu budowy.

Wyjątkiem od tej reguły jest to, że ponieważ Java nie obsługuje ani nazwanych parametrów, ani parametrów domyślnych, czasami może być nieporęczne zaprojektowanie klasy, która gwarantuje prawidłowy obiekt za pomocą przeciążonych konstruktorów. Nie tyle w przypadku dwóch własności, ale jest też coś, co należy powiedzieć o spójności, jeśli ten wzorzec jest częsty z podobnymi, ale większymi klasami w innych częściach twojego kodu.


Ciekawe, że w dyskusjach dotyczących zmienności nie rozpoznaje się związku między niezmiennością a sposobami bezpiecznego ujawnienia lub udostępnienia obiektu. Odwołanie do głęboko niezmiennego obiektu klasy można bezpiecznie udostępnić niezaufanemu kodowi. Odwołanie do wystąpienia klasy podlegającej mutacji może być udostępnione, jeśli każdemu, kto posiada odwołanie, można ufać, że nigdy nie zmodyfikuje obiektu ani nie narazi go na kod, który mógłby to zrobić. Odwołanie do instancji klasy, która może być zmutowana, zasadniczo nie powinno być w ogóle udostępniane. Jeśli tylko jedno odniesienie do obiektu istnieje w dowolnym miejscu we wszechświecie ...
supercat,

... i nic nie zapytało o „kod skrótu tożsamości”, nie użyło go do zablokowania lub w inny sposób uzyskało dostęp do jego „ukrytych Objectfunkcji”, a następnie bezpośrednia zmiana obiektu nie byłaby semantyczna niczym nadpisanie odwołania odniesieniem do nowego obiektu, który był identyczne, ale dla wskazanej zmiany. Jedną z największych słabości semantycznych w Javie, IMHO, jest to, że nie ma środków, za pomocą których kod może wskazywać, że zmienna powinna być jedynym nie efemerycznym odniesieniem do czegoś; referencje powinny być możliwe do przekazania tylko do metod, które nie mogą zachować kopii po powrocie.
supercat

0

Mogę mieć zbyt niski widok tego i prawdopodobnie dlatego, że używam C i C ++, co nie sprawia, że ​​jest to tak proste, aby wszystko było niezmienne, ale widzę niezmienne typy danych jako szczegół optymalizacji w celu napisania więcej wydajne funkcje pozbawione efektów ubocznych i bardzo łatwo udostępniać funkcje takie jak cofanie systemów i nieniszcząca edycja.

Na przykład może to być niezwykle kosztowne:

/// @return A new mesh whose vertices have been transformed
/// by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);

... jeśli Meshnie został zaprojektowany jako trwała struktura danych, a zamiast tego był typem danych, który wymagał pełnego skopiowania (który może obejmować gigabajty w niektórych scenariuszach), nawet jeśli wszystko, co będziemy robić, to zmienić część (jak w powyższym scenariuszu, w którym modyfikujemy tylko pozycje wierzchołków).

Właśnie wtedy sięgam po niezmienność i projektuję strukturę danych, aby umożliwić niezmodyfikowane części jej płytkiego kopiowania i zliczania referencji, aby umożliwić powyższą funkcję w sposób względnie wydajny bez konieczności głębokiego kopiowania całych siatek wokół, a jednocześnie móc pisać funkcja jest wolna od skutków ubocznych, co znacznie upraszcza bezpieczeństwo wątków, bezpieczeństwo wyjątków, możliwość cofnięcia operacji, zastosowania jej w sposób nieniszczący itp.

W moim przypadku jest to zbyt kosztowne (przynajmniej z punktu widzenia produktywności), aby uczynić wszystko niezmiennym, więc zapisuję to dla klas, które są zbyt drogie, aby je w całości skopiować. Te klasy są zwykle mocnymi strukturami danych, takimi jak siatki i obrazy, i generalnie używam modyfikowalnego interfejsu, aby wyrazić zmiany w nich poprzez obiekt „konstruktora”, aby uzyskać nową niezmienną kopię. I nie robię tego tak bardzo, aby uzyskać niezmienne gwarancje na centralnym poziomie klasy, ale pomagając mi korzystać z klasy w funkcjach, które mogą być wolne od skutków ubocznych. Moje pragnienie, aby Meshpowyższe było niezmienne, nie polega bezpośrednio na tym, aby siatki były niezmienne, ale aby umożliwić łatwe pisanie funkcji wolnych od efektów ubocznych, które wprowadzają siatkę i generują nową bez płacenia ogromnej pamięci i kosztów obliczeniowych w zamian.

W rezultacie mam tylko 4 niezmienne typy danych w całej mojej bazie kodu i wszystkie one są potężnymi strukturami danych, ale używam ich bardzo, aby pomóc mi pisać funkcje wolne od skutków ubocznych. Ta odpowiedź może mieć zastosowanie, jeśli, tak jak ja, pracujesz w języku, który nie sprawia, że ​​wszystko staje się niezmienne. W takim przypadku możesz skupić się na tym, aby większość funkcji unikała skutków ubocznych, w którym to momencie możesz chcieć uczynić wybrane struktury danych niezmiennymi, typy PDS, jako szczegół optymalizacji, aby uniknąć kosztownych pełnych kopii. Tymczasem kiedy mam taką funkcję:

/// @return The v1 * v2.
Vector3f vec_mul(Vector3f v1, Vector3f v2);

... nie mam więc pokusy, aby wektory były niezmienne, ponieważ są wystarczająco tanie, aby po prostu skopiować je w całości. Nie można uzyskać żadnej korzyści w zakresie wydajności, zmieniając wektory w niezmienne struktury, które mogą płytko kopiować niezmodyfikowane części. Takie koszty przeważałyby nad kosztem kopiowania całego wektora.


-1
Foo a = new Foo();
a.setProperty1("asdf");
a.setProperty2("bcde");

Jeśli Foo ma tylko dwie właściwości, łatwo jest napisać konstruktor, który przyjmuje dwa argumenty. Załóżmy jednak, że dodajesz inną właściwość. Następnie musisz dodać kolejny konstruktor:

public Foo(String a, String b, SomethingElse c)

W tym momencie jest to nadal możliwe do zarządzania. Ale co jeśli jest 10 nieruchomości? Nie chcesz konstruktora z 10 argumentami. Za pomocą wzorca konstruktora można konstruować instancje, ale to zwiększa złożoność. Do tego czasu będziesz myślał „dlaczego nie dodałem seterów dla wszystkich właściwości, tak jak robią to zwykli ludzie”?


4
Czy konstruktor z dziesięcioma argumentami jest gorszy niż NullPointerException, który występuje, gdy właściwość 7 nie została ustawiona? Czy wzorzec konstruktora jest bardziej złożony niż kod do sprawdzania niezainicjowanych właściwości w każdej metodzie? Lepiej raz sprawdzić, kiedy obiekt zostanie zbudowany.
kevin cline

2
Jak powiedziano kilkadziesiąt lat temu dokładnie w tym przypadku: „Jeśli masz procedurę z dziesięcioma parametrami, pewnie coś przegapiłeś”.
9000

1
Dlaczego nie chcesz konstruktora z 10 argumentami? W którym momencie narysujesz linię? Myślę, że konstruktor z 10 argumentami to niewielka cena za korzyści wynikające z niezmienności. Dodatkowo albo masz jedną linię z 10 rzeczami oddzielonymi przecinkami (opcjonalnie podzielonymi na kilka linii, a nawet 10 linii, jeśli wygląda lepiej), albo masz 10 linii, gdy konfigurujesz każdą właściwość osobno ...
Ricket

1
@Ricket: Ponieważ zwiększa to ryzyko umieszczenia argumentów w niewłaściwej kolejności . Jeśli używasz seterów lub wzorca konstruktora, jest to mało prawdopodobne.
Mike Baranczak

4
Jeśli masz konstruktor z 10 argumentami, być może nadszedł czas, aby pomyśleć o ich zamknięciu w klasie (lub klasach) i / lub krytycznym spojrzeniu na projekt klasy.
Adam Lear
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.