Immutable Class

Cover Image for Immutable Class
Paweł
Paweł

Dzisiaj wjeżdża kolejny klasyk z rozmów kwalifikacyjnych, czyli klasy niemutowalne. Czym dokładnie są? Jak mogą pomóc Ci w Twojej pracy? Tego dowiesz się z tego wpisu!

Niezmienność

Klasy niemutowalne to klasy, których instancje nie mogą zmieniać swojego stanu po utworzeniu. Oznacza to, że po zainicjowaniu obiektu, nie można modyfikować jego pól ani żadnej innej zawartości. Dzięki temu mamy gwarancję, że będzie się on zachowywał tak samo przez cały okres swojego życia.

Kojarzysz klasę String ze standardowej biblioteki Javy?

Na 99.99% tak.

Jest to chyba najpopularniejsza niemutowalna klasa, której programiści używają na co dzień. Oczywiście nie jest ona jedyna, jest ich więcej, ale nie ma sensu wymieniać ich wszystkich.

Korzyści

Być może zastanawiasz się jakie korzyści płyną ze stosowania takich klas w swoim kodzie. Najważniejsze z nich to:

1. Bezpieczeństwo w środowisku wielowątkowym: Kiedy obiekt jest niezmienny, nie może być modyfikowany przez wiele wątków równocześnie. Eliminuje to potrzebę synchronizacji, co sprawi, że kod będzie wydajniejszy i bardziej bezpieczny w środowisku wielowątkowym.

2. Cache’owanie i Optymalizacja: Obiekty niemutowalne mogą być cache'owanie i ponownie wykorzystywane, ponieważ ich wartości nigdy się nie zmieniają. Poprawi to wydajność, zwłaszcza w przypadku, gdy te same obiekty są często używane lub przekazywane.

3. Brak efektów ubocznych (side-effects): Dzięki klasom niemutowalnym nie trzeba martwić się nieoczekiwaną zmianą stanu jej obiektów. Operacje na nich nie zmieniają ich stanu, co zapewnia przewidywalne zachowanie i bezpieczeństwo działania takiego kodu

Implementacja

Przejdźmy krok po kroku jak zaimplementować klasę immutable w języku Java:

  1. Deklarujemy klasę jako final:
public final class ImmutableClass { ... }

Dzięki oznaczeniu klasy w taki sposób nie będzie mogła być rozszerzana. Ewentualnie jako alternatywne rozwiązanie możemy zadeklarować wszystkie konstruktory jako private lub package private , a następnie dodać static factory method, którą opisywaliśmy tutaj.

  1. Oznaczamy wszystkie pola jako final oraz private:
public class ImmutableClass { private final int field1; private final String field2;}

W ten sposób unikniemy możliwości przypisania nowej referencji do pola. Umożliwi to bezpieczne przekazywanie go między wątkami bez konieczności synchronizacji. Private daje nam pewność, że klient takiej klasy nie dostanie się w niepowołany sposób do tych pól.

  1. Nie dostarczamy żadnych metod umożliwiających zmianę stanu obiektu.

  2. Deklarujemy konstruktor przyjmujący wszystkie argumenty:

public class ImmutableClass { private final int field1; private final String field2; public ImmutableClass(int field1, String field2) { this.field1 = field1; this.field2 = field2; } }
  1. W przypadku, gdy w klasie występują pola, których typem jest customowa mutowalna klasa lub jakaś kolekcja, powinniśmy zapewnić, że klasy klientów, nie otrzymają ich oryginalnej referencji. Możemy to zrobić w następujący sposób:
public class ImmutableClass { private final int field1; private final String field2; private final CustomObject field3; private final List<CustomObject> field4; public ImmutableClass(int field1, String field2, CustomObject field3, List<CustomObject> field4) { this.field1 = field1; this.field2 = field2; this.field3 = new CustomObject(field3); this.field4 = ImmutableList.copyOf(field4); } public int getField1() { return field1; } public String getField2() { return field2; } public CustomObject getField3() { return new CustomObject(field3); } public List<CustomObject> getField4() { return ImmutableList.copyOf(field4); } }

Co tutaj zrobiliśmy?

Zarówno w konstruktorze jak i w getterach na nowo utworzyliśmy obiekt dla field3. Dodatkowo dla field4, które jest listą użyliśmy statycznej metody copyOf wbudowanej w standardową bibliotekę Javy od wersji 10+.

Jeżeli używasz starszej wersji możesz wspomóc się Guavy. Takie podejście zapewnia nam, że gdy klient nie będzie w stanie dobrać się do oryginalnej referencji i pozmieniać coś czego byśmy nie chcieli.

Trudne?

Chyba nie, ale dla pewności podsumujmy sobie jeszcze raz co musi mieć niezmienna klasa:

1. Nie ma możliwości dziedziczenia po niej: zapobiega to modyfikacjom w wyniku rozszerzania przez inne klasy.

2. Wszystkie jej pola są oznaczone jako finalne i prywatne: wartości tych pól nie mogą zostać zmienione po ich ustawieniu w konstruktorze, a także uniemożliwia to bezpośredni dostęp do tych pól przez inne klasy.

3. Nie posiada żadnych setterów, ani innych metod pozwalających na zmianę stanu: Zapewnia to, że po utworzeniu obiektu jego stan nie będzie podlegał zmianie.

4. Posiada konstruktor umożliwiający inicjalizacje wszystkich pól klasy: umożliwia ustawienie wartości wszystkich pól w momencie tworzenia obiektu.

5. Zapewnia niezmienność swoich składowych: Jeśli zawiera pola, których typem jest kolekcja lub customowy obiekt, należy zadbać o to, aby klasy klienckie nie otrzymywały bezpośredniej referencji do tych składników.

Problemy

Wiadomo, nie ma róży bez kolców, jakie są wady stosowania** Immutable Class**?

Ponieważ niemodyfikowalne klasy nie mogą zmieniać swojego stanu, każda operacja, która wydaje się zmieniać ich obiekt, tak naprawdę tworzy nowy. W skrajnych przypadkach może to prowadzić do zwiększonego zużycia pamięci, szczególnie gdy operacje są wykonywane na dużych zbiorach danych. Może się to przekładać na problemy z wydajnością.

Tak jak zawsze powtarzamy, należy zachować zdrowy rozsądek i przemyśleć czy użycie takiej klasy nie wpłynie negatywnie na naszą aplikację.

Podsumowanie

Teraz już wiesz czym jest i jak zaimplementować Immutable Class. Korzystanie z tej koncepcji przynosi wiele korzyści, m.in zapewnienie bezpieczeństwa w wielowątkowym środowisku. Na pewno jest to coś o czym powinien wiedzieć programista pracujący z paradygmatem obiektowym. Umiejętne wykorzystanie niemodyfikowalnych klas może znacząco usprawnić projektowanie i utrzymanie aplikacji.