Hej, dzisiaj kontynuacja poprzedniego wpisu o niezmiennych obiektach.
Jako programiści .Net już teraz pracujemy z niezmiennymi obiektami na co dzień. Przykładem może być String albo DateTime. Niestety sam .Net nie pozwala nam jeszcze na tworzenie takich obiektów w bardzo łatwy sposób, np za pomocą samej adnotacji na klasie. Są plany aby to zmienić w kolejnych wersjach językach, ale zobaczymy co z tego i kiedy wyjdzie. W tym momencie, aby stworzyć taki obiekt, sami musimy zadbać o jego niezmienność. Co też nie jest trudne.
Przykładowa niezmienna klasa może wyglądać w ten sposób:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public class ImmutableClass { readonly private int someNumber; readonly private int secondField = 5; public int ExampleProperty { get { return someNumber; } } public ImmutableClass() { someNumber = 0; } public ImmutableClass(int number) { someNumber = number; } public ImmutableClass AddToSomeNumber(int numberToAdd) { int newSomeNumber = someNumber + numberToAdd; return new ImmutableClass(newSomeNumber); } } |
Na co musimy zwrócić uwagę to:
- Żadne pole nie może być zmienione po utworzeniu klasy. W tym przykładzie zapewniamy to sobie przez oznaczenie pól jako readonly. Jak zapewne wiesz, zapewnia to, że do pola nie możemy niczego przypisać poza konstruktorem, lub inicjalizując je. Tutaj widzimy wykorzystanie obu tych sposobów. Nie musisz oznaczać pół jako prywatne, mogą równie dobrze być publiczne. Oczywiście wtedy osłabia to enkapsulację. Do tego, dodałem przykładową właściwość pozwalającą udostępnić jedno z pól na zewnątrz klasy. Osłabia to tak samo enkapsulację jak zrobienie pola publicznym i nie wiele w tym wypadku zmienia poza samym sposobem dostępu. Jak widać, jeśli potrzebujesz, nic nie stoi na przeszkodzie by utworzyć domyślny bezparametrowy konstruktor.
- Rozszerzenie poprzedniego punktu: jeśli polem klasy jest klasa, to też idelanie powinna być niezmienna. Czyli albo używamy typów prostych, albo innych (bądź naszej) klas niezmiennych. Inaczej całą niezmienność szlag może trafić. Za chwilę do tego wrócimy.
- Jeśli chcemy „zmienić obiekt”, co jest bardzo prawdopodobne, tworzymy metodę zwracającą nowy obiekt naszej klasy, na podstawie obecnego. Tutaj trywialny przykład ze zwiększaniem wartości jednego z pól.
- Jeśli używamy dziedziczenia, to wszystkie klasy w hierarchii powinny być niezmienne. Czyli dziedziczymy tylko po klasie niezmiennej i jeśli rozszerzamy klasę niezmienną to też nowa klasa musi być niezmienna. Konsekwencje niezastosowania się do tego są dość oczywiste: jeśli dziedziczymy po normalnej klasie – to nasza już nie jest tak naprawdę niezmienna. W drugą stronę konsekwencje są trochę mniejsze… tyle, że oczywiście uzyskamy klasę która nie jest niezmienna. Klasa z której dziedziczymy pozostanie niezmienna. Ale zaburzy to to prostotę naszego rozwiązania, i zmusi do niepotrzebnego myślenia która klasa jest a która nie jest niezmienna, co może też wpływać na sens stosowania w ogóle niezmienności w klasie bazowej. Możemy tutaj też, żeby zabezpieczyć się przez dziedziczenie po naszej niezmiennej klasie, zamknąć ją na tę opcję. Będzie to miało większy sens przy tworzeniu jakiejś biblioteki, niż przy klasach używanych w jednym zespole. Dlatego, w tym przykładzie nie zdecydowałem się na oznaczenie klasy jako sealed.
Rozważmy teraz przykład o którym pisałem w punkcie 2. Załóżmy że mamy takie o to proste klasy, z której jedna próbuje być niezmienną:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public class NotSoImmutable { public readonly MutableClass nestedMutableClass; public NotSoImmutable(MutableClass param) { nestedMutableClass = param; } } public class MutableClass { public string notEncapsulatedField; private string updatableField; public MutableClass(string param, string otherParam) { notEncapsulatedField = param; updatableField = otherParam; } public void Update(string param) { updatableField = param; } } |
Jak widzisz, pierwsza klasa stara się być niezmienna, ale taka nie jest. Mimo że spełnia wszystkie wymogi, oprócz tego jednego, że posiada w sobie zmienny obiekt. Niestety, jak widzisz ma on pole które jest dostępne publicznie i w każdej chwili może być zmienione. Przez co cała klasa traci już status niezmiennej. Jeszcze gorzej, że jest ona publicznie dostępna i w każdej chwili używania obiektu klasy NotSoImmutable ktoś może ją wyciągnąć i zmodyfikować. tutaj kilka przykładów jak może do tego dojść:
1 2 3 4 5 6 7 8 9 |
var mutableObject = new MutableClass("string", "differentString"); var notSoImmutableClass = new NotSoImmutable(mutableObject); //zmodyfikujemy naszą klasę tak: notSoImmutableClass.nestedMutableClass.notEncapsulatedField = "change"; //lub tak notSoImmutableClass.nestedMutableClass.Update("change"); //albo jeszcze tak, bo ciągle mamy referencję: mutableObject.notEncapsulatedField = "change"; |
Jak widać klasa NotSoImmutable wcale nie jest niezmienna. Spróbujmy więc, zrobić to inaczej:
1 2 3 4 5 6 7 8 9 |
public class NotSoImmutableTakeTwo { readonly private MutableClass nestedMutableClass; public NotSoImmutableTakeTwo(string param, string otherParam) { nestedMutableClass = new MutableClass(param, otherParam); } } |
Teraz nie ma możliwości na zmianę wartości w naszej klasie zagnieżdżonej, gdyż nie jest dostępna na zewnątrz. Nie ma też do niej referencji przy tworzeniu klasy niezmiennej, bo nasza klasa zagnieżdżona tworzona jest w konstruktorze. To też jest jakieś wyjście. Niestety, nie jest odporne na błędy przy modyfikacji klasy NotSoImmutableTakeTwo. Następny programista może dodać konstruktor z parametrem, bądź upublicznić nasze pole na zewnątrz. Ale równie dobrze, mając doskonale niezmienną klasę, programista może łatwo tę niezmienność złamać usuwając pola readonly. Także jest to jakieś rozwiązanie pośrednie, ale znów nie jest doskonałe i sam preferowałbym podejście czyste, gdy wszystkie klasy w hierarchii dziedziczenia oraz klasy będące w środku naszych niezmiennych same były niezmienne. Jest to łatwiejsze do ogarnięcia i przez to bezpieczniejsze. Ale życie nie zawsze jest takie proste 🙂