Immutable

Immutable, czyli niezmienne obiekty – wady, zalety oraz kilka praktycznych przykładów.

Zastanawiałeś/zastanawiałaś się kiedyś, jak na nasze życie wpływają różne ograniczenia?
Czy zawsze są one tak złe, jak na początku nam się to wydaje?

Spokojnie! Nie chcę Cię teraz umoralniać – związek z programowaniem już za chwilę 🙂

Przypomnij sobie, jak w dzieciństwie słyszałeś od rodziców:
nie dotykaj tego, bo to gorące! lub: nie wychylaj się, bo spadniesz i inne podobne.

Dziś z perspektywy czasu i tego, że jestem ojcem, patrzę na tego typu ograniczenia trochę inaczej – lub zwyczajnie sam je nakładam…
Mam jednak świadomość, co się za nimi kryje.
Wiem, że nie są to tylko ograniczenia, które mają uprzykrzyć życie.
Wiem, że jeżeli nie będziemy ich przestrzegać, to możemy coś stracić lub może nas zwyczajnie zaboleć…
Ewentualnie w drugą stronę: jeżeli będziemy ich przestrzegać, to możemy coś zyskać.

W programowaniu też mamy takie ograniczenia. Wiele z nich na pierwszy rzut oka wydaje się bezsensowna i niestety staramy się je ominąć, bo przecież tylko utrudniają nam życie…

Widzisz już związek?

Niech pierwszy rzuci kamieniem ten, kto choć raz nie:

  • skorzystał z git push force;
  • dostał się do pola klasy przy pomocy refleksji;
  • lub zmienił modyfikator dostępu do metody lub klasy na mniej restrykcyjny.

Nie ma w tym nic złego, jeżeli rzeczywiście wiemy, co robimy, i świadomie „łamiemy te zakazy”.

Problem pojawia się jednak wtedy, gdy nie znamy konsekwencji swojego działania, czyli postępujemy podobnie jak dzieci – chcemy osiągnąć chwilową przyjemność, jednocześnie nawet nie zastanawiając się, jakie to może mieć skutki.

W dzisiejszym wpisie przyjrzymy się bliżej jednemu z takich ograniczeń – niemodyfikowalnym obiektom i zastanowimy się, co dzięki ich wykorzystaniu możemy zyskać jako programiści.

Z tego odcinka dowiesz się:

  • co to jest immutable object;
  • jak efektywnie korzystać z gotowych klas implementujących ten wzorzec;
  • jak samodzielnie zaimplementować immutable object;
  • oraz jakie są wady i zalety niemodyfikowalnych obiektów.
[SprawnyProgramista_intro]

Immutable object – czyli w dosłownym tłumaczeniu niezmienny obiekt. Jest to wzorzec projektowy, w którym finalny stan obiektu ustalany jest już podczas jego tworzenia i przez cały okres jego życia nie może zostać zmieniony. W efekcie tego, gdybyśmy chcieli wprowadzić jakąś zmianę w jego stanie, trzeba będzie utworzyć nowy obiekt z jego zmodyfikowaną wartością.

Mutable object – zmienny obiekt

Żeby było to bardziej zrozumiałe, zacznijmy od wprowadzenia pojęcia modyfikowalnych obiektów (ang. mutable objects), które są przeciwieństwem niemodyfikowalnych obiektów (ang. immutable objects).

Poniżej jest przykładowa klasa reprezentująca osobę posiadającą swoje imię oraz wiek.

class Person {
	private String name;
	private int age = 0;

	public Person(String name) {
		this.name = name;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}
}

Obiekty tego typu możemy utworzyć, przypisując im jednocześnie jakiś inicjalny stan, a następnie zmodyfikować (np. korzystając z setterów).

Person person = new Person("Tomek");
person.setAge(18);

Mimo iż podczas tworzenia obiektu person przypisaliśmy domyślny wiek (age = 0), to nie mieliśmy problemu, żeby go później zmodyfikować.

Tego typu obiekty są bardzo powszechne i najczęściej wykorzystywane przez developerów – co nie zawsze jest dobrym pomysłem, ale o tym za chwilę.

Immutable object – niezmienny obiekt

Raz utworzony immutable object nigdy nie zmieni już swojej wartości.

Co to tak naprawdę dla nas znaczy?

Takie zachowanie klasy można porównać do swego rodzaju odlewu w formie. Jeżeli potrzebujemy innego odlewu, trzeba przygotować nową formę i jeszcze raz zrobić odlew.

UWAGA: chodzi tutaj przede wszystkim o stan obiektu widoczny z zewnątrz. Może zajść taka sytuacja, że obiekt zmodyfikuje wewnętrznie jakieś swoje parametry, jednak póki nie zmienia to stanu widocznego z zewnątrz, dalej rozpatrujemy taki obiekt jako immutable.

Immutable String

Najbardziej znaną klasą niemodyfikowalną jest standardowy String wykorzystywany do przechowywania ciągu znaków.

Zobaczmy to na przykładzie:

Klasa String nie posiada żadnego settera ani innej metody, którą moglibyśmy zmienić jego wartość. Dlatego chcąc zmienić jego stan, tworzymy nowy obiekt oraz modyfikujemy referencje wskazującą na nasz obiekt.

W powyższym przykładzie utworzyliśmy nowy obiekt string o wartości „Java 8” i ustawiliśmy na niego zmienną referencyjną str.

➡ ZOBACZ 👉: String – najważniejszy typ danych, niezmienność stringów

Zobaczmy to na jeszcze jednym przykładzie. Poniższy fragment kodu wyświetli informację o miejscu w pamięci, w którym znajdują się poszczególne obiekty.

String str = "Java";
System.out.println("String str = \"Java\" => " + System.identityHashCode(str));
System.out.println("\"Java\" => " + System.identityHashCode("Java"));
System.out.println("new String(\"Java\") => " + System.identityHashCode(new String("Java")));

str += " 8";
System.out.println("str += \" 8\" => " + System.identityHashCode(str));

Z logów wynika, że każdorazowe utworzenie stringa jako literał przez wykorzystanie cudzysłowu zwraca nam ten sam obiekt – dzieje się tak, ponieważ Java wewnętrzne wykorzystuje pulę stringów.
Natomiast utworzenie nowego stringa przez słowo kluczowe new, podobnie jak konkatenacja stringów, tworzy nowe obiekty.

String str = "Java"     => 1513712028
"Java"                  => 1513712028
new String("Java")      => 1018547642
str += " 8"             => 1456208737

Alternatywą dla niemodyfikowalnego stringa są jego modyfikowalne odpowiedniki StringBuilder oraz StringBuffer. Warto rozważyć ich wykorzystanie, jeżeli wiemy, że nasz ciąg znaków będzie często modyfikowany. O obu klasach możesz przeczytać w poniższym artykule:

➡ ZOBACZ 👉: StringBuilder: czy zawsze taki szybki? | String vs StringBuilder vs StringBuffer

Równie dobrym przykładem immutable objects są obiekty klas opakowujących typy proste, takie jak Integer, Double, Float czy Boolean.

Implementacja Immutable Object w Javie

Wiemy już, jak korzystać z gotowych klas implementujących wzorzec: Immutable Object, przejdźmy teraz dalej i sami spróbujemy przygotować taką klasę.

Implementacja Immutable Object w Javie

Implementacja Immutable Object w Javie

W dokumentacji Oracle możemy przeczytać zalecenia odnośnie strategii implementacji niemodyfikowalnych obiektów. Nie jest to nic trudnego, trzeba jednak pamiętać o kilku poniższych zasadach:

  • Pola klasy oznaczone jako private i final 
    Blokujemy w ten sposób wszystkie pola przed bezpośrednim dostępem z zewnątrz klasy oraz pilnujemy samych siebie przed omyłkową ich modyfikacją w późniejszym kodzie.
    Wartość wszystkich parametrów musi zostać ustalona już w konstruktorze.
  • Brak setterów
    Rezygnujemy z pisania setterów lub innych metod modyfikujących stan pól – i tak byłoby to niemożliwe, ponieważ wszystkie pola oznaczone są już jako final.
  • Klasa final
    Dzięki temu nie będzie możliwości dziedziczenia po naszej klasie, a co za tym idzie – modyfikacji jej zachowania.
  • Opcjonalnie konstruktor prywatny + publiczna metoda fabrykująca
    Prywatny konstruktor zablokuje możliwość tworzenia obiektów tej klasy z zewnątrz, dzięki czemu publiczna metoda fabrykująca (wzorzec projektowy ang. factory method) będzie jedynym sposobem na utworzenie takiego obiektu.
  • Poszczególne pola naszej klasy też muszą być immutable.
    Wszystkie typy pól klasy również muszą być immutable, w przeciwnym wypadku będzie możliwość ich edycji.

Poniżej przykładowa implementacja immutable object reprezentująca adres.

public final class Address {
	private final String street;
	private final String city;

	public Address(String street, String city) {
		this.street = street;
		this.city = city;
	}

	public String getStreet() {
		return street;
	}

	public String getCity() {
		return city;
	}
}

Uwaga na odwołania do innych mutowalnych obiektów (Immutable Map, Immutable List)

Trochę trudniej sprawa wygląda, jeżeli zamierzamy zrobić odwołanie do innych mutowalnych obiektów, np. gdy chcemy przechować listę obiektów z wykorzystaniem kolekcji: java.util.List.

W tym wypadku nie wystarczy samo oznaczenie pola jako final, ponieważ stan takiej kolekcji dalej będzie mógł być zmodyfikowany. Nawet jeżeli kolekcja będzie przechowywała niemodyfikowalne obiekty, to będzie można np. dodać do niej nowy obiekt lub go usunąć. Żeby się przed tym ustrzec, musimy zrobić głęboką kopię obiektu. Ponieważ przechowamy lokalnie swoją prywatną kopię takiej kolekcji, to mamy pewność, że nikt już jej nie zmodyfikuje.

Możemy to osiągnąć na dwa sposoby:

  • kopiowanie w konstruktorze
    this.items1 = Collections.unmodifiableList(items1);
  • kopiowanie w getterach
    return Collections.unmodifiableList(items1);

Oba rozwiązania sprowadzają się do tego samego – poza klasę nie możemy wystawić referencji do pierwotnej kolekcji, ponieważ będzie to groziło jej modyfikacją.

public final class Address {
	/* street, city declaration */

	private final List<String> items1;
	private final List<String> items2;

	public Address(String street, String city, List<String> items1, List<String> items2) {
		this.street = street;
		this.city = city;
		this.items1 = Collections.unmodifiableList(items1);
		this.items2 = items2;
	}

	/* street, city getters */

	public List<String> getItems1() {
		return items1;
	}

	public List<String> getItems2() {
		return Collections.unmodifiableList(items1);
	}
}

Podobne rozwiązanie trzeba zastosować w przypadku innych kolekcji, map oraz każdego modyfikowalnego obiektu, który chcemy przechować.

Alternatywna implementacja z wykorzystaniem biblioteki Immutables

W celu implementacji immutable object możemy również pokusić się o wykorzystanie gotowych rozwiązań, takich jak biblioteka Immutables.
Może to pozwolić na znaczne uproszczenie naszego kodu – więcej na ten temat przeczytasz w podlinkowanym wpisie.

@Value.Immutable
public abstract class UserValueImmutable {
 
   abstract String getName();
   abstract Integer getAge();
}

Immutable – zalety

Zobaczmy teraz główne powody, dla których warto zainteresować się niemodyfikowalnymi obiektami.

  1. Prostota – łatwiejsze wykorzystanie oraz testowanie
    Z punktu widzenia użytkownika bardzo łatwo korzysta się z takich obiektów oraz je testuje – mamy pewność, że ich wartość się nie zmieni.
  2. Większa stabilność i łatwość potencjalnego szukania błędów
    Założenie niemodyfikowalności obiektów pozwala znacząco uprościć logikę w kodzie (np. nie musimy przejmować się, że obiekt przekazany przez parametr funkcji zostanie zmodyfikowany). Łatwiejszy kod oznacza większą stabilność całego rozwiania oraz mniej błędów.
  3. Możliwość keszowania (ang. cache) takich obiektów
    Niezmienność obiektów ma jeszcze jedną bardzo ważną cechę – możemy poprawić wydajność aplikacji przez keszowanie takich obiektów. Najpopularniejszym przykładem takiego keszowania jest wbudowana pula stringów (ang. String Pool).
    ➡ ZOBACZ 👉: String – najważniejszy typ danych, pula stringów.
  4. Możliwość bezpiecznego wykorzystania w Setach oraz Mapach jako klucze.
    Zmiana wartości obiektu może zmienić jego hashCode, a co za tym idzie – taki obiekt mógłby się zagubić w standardowych kolekcjach Java.
    Więcej przeczytasz o tym w poniższym wpisie:
    ➡ ZOBACZ 👉: hashCode i equals – co grozi Ci za złamanie kontraktu między nimi?
  5. Thread-safe – bezpieczniejsze i wydajniejsze programowanie wielowątkowe.
    Ponieważ stan obiektów immutable się nie zmienia, mamy pewność, że wszystkie wątki widzą to samo. Dzięki temu stosunkowo niewielkim kosztem możemy zrezygnować z czasochłonnej i problematycznej synchronizacji oraz zwiększamy bezpieczeństwo programowania wielowątkowego i rozproszonego.
  6. Wbudowana optymalizacja Garbage Gollector.
    GC jest odpowiednio zoptymalizowany do sprawnego odśmiecania małych i krótko żyjących obiektów z pamięci.
  7. Lepsze odzwierciedlenie rzeczywistości.
    Wiele elementów otaczającego nas świata z natury jest niezmienna, dlatego może być to naturalne wymaganie przy budowaniu modelu domenowego.

Immutable – wady

Przyjrzyjmy się teraz potencjalnym wadom immutable objects. Potencjalnym, dlatego że wiele z nich należałoby raczej rozpatrywać jako ich specyfikę niż rzeczywistą wadę.

  1. Trudniejsza implementacja
    Czasem jako argument przeciwko temu rozwiązaniu podaje się trudniejszą implementację. Osobiście uważam, że bardziej pasowałoby tu określenie: inna implementacja, niż trudniejsza.
  2. Konieczność inicjalizowania wszystkich własności obiektu w konstruktorze
    Może to być rzeczywiście problematyczne, szczególnie jeżeli mielibyśmy bardziej rozbudowany obiekt, z dużą ilością pól. W takich sytuacjach jednak zazwyczaj nie stosuje się immutable objects lub można skorzystać ze wzorca projektowego (ang. design pattern) budowniczy (ang. builder), który znacząco ułatwi tworzenie takiego obiektu.
  3. Każda próba zmiany obiektu wiąże się z koniecznością utworzenia nowego obiektu.
    Potencjalnie może wiązać się to ze zwiększonym zużyciem pamięci na przechowanie tych obiektów oraz CPU na ich obsługę, jednak współczesne GC są bardzo dobrze zoptymalizowane pod tym kątem.

Kiedy korzystać z immutable object?

Kiedy w takim razie powinniśmy korzystać z immutable objects, a kiedy z mutable objects?

Każdy wypadek oczywiście powinien być rozpatrywany indywidualnie, jednak można wyróżnić kilka grup problemów, przy których rozwiązaniu immutable objects może być szczególnie pomocne.

  • programowanie wielowątkowe;
  • wykorzystanie obiektów jako różnego rodzaju klucze, np. w mapach;
  • obiekt ma być typowym value object – czyli niewielkim prostym obiektem, który nie ma jednoznacznie określonej tożsamości (np. ID, pesel itp.).

Kiedy nie korzystać z immutable object?

Cytując klasyka: klasy powinny być niemodyfikowalne, chyba że jest bardzo dobry powód, żeby było inaczej. Jeżeli natomiast klasa nie może być niemodyfikowalna, to należy możliwie ograniczyć jej modyfikowalność.

Classes should be immutable unless there’s a very good reason to make them mutable… If a class cannot be made immutable, limit its mutability as much as possible.

Joshua Bloch, Effective Java

W takiej sytuacji nasuwa się pytanie: co to właściwie znaczy dobry powód?

Widzę dwa główne powody, dla których warto zastosować modyfikowalne obiekty:

  • obiekt posiada swoją „tożsamość” i reprezentuje np. osoby, rzeczy itp. Dla tego typu obiektów zmiana pewnych parametrów jest naturalna, np. osoba zmienia swój wiek, samochód zmienia prędkość itp.
  • wydajność – gdy pracujemy z dużymi obiektami, których tworzenie zajmuje dużo czasu, ich ponowne wykorzystanie może być dobrym pomysłem.

Podsumowanie i zadanie

Immutable object jest jedną z dobrych praktyk programistycznych, która potrafi nie tylko ułatwić codzienną pracę z kodem, ale również ustrzec nas przed potencjalnymi problemami w przyszłości. Zarazem jest wyjątkowo prosta do wdrożenia, dlatego warto się nią zainteresować.

Na dzisiaj to już wszystko, chciałbym jednak, żebyś spróbował przećwiczyć tę wiedzę w praktyce – korzystając z wzorca immutable object, zaimplementuj klasę Number reprezentującą liczbę całkowitą. Klasa powinna implementować przynajmniej dwie metody: Number of(int n) tworzącą nowy obiekt oraz Number add(Number number) dodającą dwa obiekty.

Number result = Number.of(1).add(Number.of(10));

Wynikami swojej pracy podziel się w komentarzu poniżej lub na naszej grupie. Przykładową implementację znajdziesz w repozytorium.

Pozdrawiam i powodzenia w realizacji zadania!

 

Dodatkowe materiały:

 

kierunek java


20+ BONUSOWYCH materiałów z programowania

e-book – „8 rzeczy, które musisz wiedzieć, żeby dostać pracę jako programista”,
e-book – „Java Cheat Sheet”,
checklista – „Pytania rekrutacyjne”
i wiele, wiele wiecej!

Jak zostać programistą

No comments
Share:

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *