O ile pojęcie niezmiennych obiektów nie brzmi zbyt znajomo, to o tyle na pewno słyszałeś o Immutable Object. I o tym właśnie będzie ten wpis.
Czym jest Immutable Object?
Tutaj nie ma żadnego haczyka i nazwa dokładnie wskazuje o czym mówimy. Immutable object jest to niezmienny obiekt, czyli taki, którego stan nie może zostać zmieniony cały okres życia obiektu. Czyli po prostu tworzymy nasz obiekt wraz ze wszystkimi wymaganymi atrybutami i żadnego z nich nie możemy zmienić. Przynajmniej w teorii :)… ale zmiany poprzez refleksje się nie liczą, więc uznajemy, iż nie mamy możliwości zmiany wartości tych atrybutów
Jakie są zalety i wady tego podejścia?
Niezmienne obiekty, jak wszystko ;), mają zalety i wady. Do najważniejszych zalet według mnie należą:
- Są łatwiejsze w użyciu i testowaniu
- Można je bezpiecznie używać w Setach lub jako klucz w Mapach
- Mogą być łatwo cachowane
- Immutable object mogą być bezpiecznie używane w programowaniu wielowątkowym. Stan tych obiektów nie może ulec zmianie, więc mamy pewność, iż każdy wątek widzi aktualny stan obiektu
Wady Immutable Object:
- Nadmiarowy kod – musimy dopisać kilka finali, ale za to pozbywamy się setterów
- Inicjalizacja wszystkich pól przez konstruktory. Ponieważ wszystkie nasze pola są oznaczone jako final, muszą więc zostać zainicjalizowane w konstruktorze. A co jak nie chcemy zawsze podawać wartości wszystkich parametrów, tylko użyć domyślnych wartości dla niektórych pól? Wtedy musimy stworzyć osobny konstruktor dla każdej kombinacji pól, którą chcemy użyć.
- Problem z wydajnością – za każdym razem, gdy chcemy wprowadzić zmianę w naszym obiekcie to sprowadza się to do utworzenia nowego obiektu. Może to być odczuwalne zarówno w czasie działania aplikacji, jak i zużyciu pamięci.
Kiedy ich używać?
Jest na pewno kilka podstawowych use-caseów, kiedy powinniśmy rozważyć użycie niezmiennych obiektów:
- programowanie wielowątkowe – o ile mamy obiekt, który ma być współdzielony pomiędzy wątkami to zdecydowanie warto rozważyć użycie immutable object
- obiekt używany jako klucz (np. w mapach) – mamy wtedy pewność, iż klucz nie zostanie zmieniony kiedy jest już w użyciu i nie będzie kolizji
- obiekt ma być typowym 'value object’ – wydaje się oczywiste
Z drugiej strony powinniśmy skłaniać się ku 'standardowym obiektom’, kiedy mamy do czynienia:
- z dużymi obiektami, których tworzenie zajmie dużo czasu i/lub pamięci
- obiektami, które posiadają 'tożsaność’, tzn. reprezentują osoby/rzeczy, dla których zmiana pewnych parametrów jest naturalna np. samochód, dla którego naturalnymi jest zmiana takich parametrów jak prędkość czy poziom paliwa.
Implementacja Immutable Object w Javie
Stworzenie Immutable Object w Javie jest dosyć proste. Wystarczy przestrzegać kilku wskazówek
- Wszystkie pola powinny posiadać modyfikatory private i final.
- Nie tworzymy setterów (wynika to zresztą z użycia modyfikatora final przy polach).
- Musimy zabezpieczyć naszą klasę, żeby nie można było po niej dziedziczyć.
- Jeżeli pola naszej klasy zawierają mutable object, wtedy musimy zabezpieczyć te obiekty przed zmianą.
Pora na przykład, który zobrazuje nam powyższe zasady.
public final class MyImmutableObject { // uzywajac final zapewniamy, ze po klasie nie mozna dziedziczyc private final String name; // wszystkie pola maja modyfikatory private i final private final int age; private final List<String> plants; private final List<Pet> pets; // uzywajac innych obiektow, musimy zapewnic ich niezmiennosc public MyImmutableObject(String name, int age, List<Pet> pets, List<String> plants) { this.name = name; this.age = age; // uzywajac list musimy pamietac o trzech rzeczach: // 1. Obiekty w listach powinny rowniez byc Immutable // 2. Stworzyc kopie przekazanej listy, zeby zabezpieczyc sie przez jej modyfikacja z zewnatrz // 3. W gecie zwracac kopie listy lub uzyc unmodifiableList lub innej listy, ktorej nie mozna edytowac this.pets = Collections.unmodifiableList(new ArrayList(pets)); this.plants = Collections.unmodifiableList(new ArrayList(plants)); } public String getName() { // zadne z pol nie ma settera return name; } public int getAge() { return age; } public List<Pet> getPets() { return pets; } public List<String> getPlants() { return plants; } }Trochę optymalizacji
Jeżeli nasz niezmienny object będzie na prawdę często używany w naszej aplikacji możemy rozważyć pewne optymalizacje.
Jedną z technik, którą możemy użyć jest Pooling. Jest on np. użyty w JVM w przypadku Stringów. Poniżej przedstawię bardzo prymitywną implementacje tego podejścia.
Na początku stwórzmy jeszcze nasz immutable object, którego użycie będizemy chcieli zoptymalizować.
public final class Person { private final String name; private final int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } }I teraz prosty mechanizm poolingu:
public class PersonPool { private static final Map<String, Map<Integer, Person>> PEOPLE = new HashMap<>(); public static Person getPerson(final String name, final int age) { if (PEOPLE.containsKey(name)) { if (PEOPLE.get(name).containsKey(age)) { // jesli w naszej mapie istnieje osoba o tej samej nazwie // i wieku to zwracamy utworzona wczesniej osobe return PEOPLE.get(name).get(age); } else { // jezeli istnieje osoba o tym samym imieniu, ale nie wieku // to tworzymy nowa osobe, dodajemy do poli i zwracamy utworzony obiekt Person p = new Person(name, age); PEOPLE.get(name).put(age, p); return p; } } else { // jezeli nie istnieje osoba o takim imieniu to rowniez tworzemy nowa osobe // i dodajemy ja do naszej poli Person p = new Person(name, age); PEOPLE.put(name, new HashMap<>()); PEOPLE.get(name).put(age, p); return p; } } }Ogólnie mówiąc ideą jest powtórne użycie wcześniej utworzonego obiektu, o ile jest taki sam jak byśmy chcieli stworzyć.
Przy używaniu poolingu powinniśmy mieć na uwadze, iż o ile w środowisku jednowątkowym powinien sprawdzić się całkiem nieźle, o tyle przy wielu wątkach narzut na synchronizacje może być znaczący.
Podsumowanie
We wpisie postarałem przedstawić się ideę niezmiennych obiektów, ich wady oraz zalety. Pokazałem również jak zaimplementować taki obiekt w Javie oraz zaproponowałem pewną optymalizacje, którą możemy użyć, aby zwiększyć wydajność naszej aplikacji w przypadku częstego korzystania z naszego immutable object.
UPDATE 14.05.2017
Kolega zwrócił mi uwagę na błąd, który był w listingu z implementacją niezmiennej klasy w Javie – teraz wszystko powinno być ok
Zwrócił również uwagę, iż warto dodać wzmiankę o poolingu w środowisku wielowątkowym – dodane
Dzięki