Niezmienne obiekty – po co nam one?

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, że 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ść, że 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 – jeżeli 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ść, że 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 🙂

  1. Wszystkie pola powinny posiadać modyfikatory private i final.
  2. Nie tworzymy setterów (wynika to zresztą z użycia modyfikatora final przy polach).
  3. Musimy zabezpieczyć naszą klasę, żeby nie można było po niej dziedziczyć.
  4. 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, jeżeli jest taki sam jak byśmy chcieli stworzyć.

Przy używaniu poolingu powinniśmy mieć na uwadze, że 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ę, że warto dodać wzmiankę o poolingu w środowisku wielowątkowym – dodane 🙂

Dzięki 🙂

← Previous post

Next post →

3 Comments

  1. „Kiedy ich używać?”
    Odpowiedź powinna brzmieć – kiedy tylko to możliwe.
    Ponadto, gdy pracujemy w pełni z niemutowalnymi obiektami, ich kopiowanie opiera się na tzw. „structural sharing”. Warto również podkreślić, że JVM bardzo dobrze sobie radzi z „short-lived” obiektami.

    Oczywiście, są wyjątki od tej reguły i wtedy zaczynamy myśleć o odpowiednikach mutowalnych, ale dopiero gdy jesteśmy przekonani, że to stanowi bottleneck aplikacji.

    Wydaje mi się również, że taka dyskusja (mutability/immutability) jest ciekawsza z punktu widzenia persystentnych struktur danych aniżeli czystych Plain Objectów.

    Niemniej jednak, post warty uwagi dla wielu osób niezaznajomionych z tematem (immutability jest rdzeniem paradygmatu funkcyjnego).

    • Łukasz Antkowiak

      Zgadzam się w pełni – powinniśmy używać kiedy to możliwe. Przynajmniej w idealnym świecie 😉

  2. Jeżeli ta pula ma działać w środowisku wielowątkowym to myślę, że warto użyć wewnętrznie synchronizowanej implementacji mapy np. ConcurrentHashMap. Niemutowalność elementów mapy nie zapewnia nam bezpieczeństwa ze względu na wątki.

Dodaj komentarz

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