Czym jest niezmienność (immutability) obiektów i kiedy warto z niej korzystać?

Dziś skupimy się na niezmienności, niezmienialności, niemutowalności (i jeszcze pewnie kilka innych przymiotników z nie), ehh wolę po prostu immutability obiektów. Trend ten jest dosyć dobrze znany programistom języków funkcyjnych, ale wraz z upływem czasu podobną technikę stosuje się w coraz większej ilości języków.

Co to znaczy, że obiekt jest immutable?

Niezmienialny obiekt to taki, który raz zainicjalizowany nie zmieni swojego stanu. O co dokładnie chodzi? Mamy gwarancję, że wszystkie dane (pola) w konkretnym obiekcie nie ulegną zmianie. Żeby zmodyfikować jakieś wartości, trzeba utworzyć całkowicie nowy obiekt.

Czy znasz jakiś niemutowalny obiekt?

W większości języków programowania idealnym przykładem obiektu, który nie zmienia się w czasie jest String. Tak, tak, za każdym razem kiedy używamy operatora += albo dodajemy jakieś znaki, tak na prawdę w środku tworzony jest całkowicie nowy obiekt. Dlatego też, jeśli budujesz nowy obiekt typu String z kilku kawałków (np. ładując coś z pliku) warto używać StringBuildera.

Po co tego używać?

Zakładam, że wiesz co to programowanie obiektowe, popatrz więc na obiekty w otaczającym nas świecie. Czy możesz skategoryzować obiekty na mutable oraz immutable? Poniżej kilka przykładów:

Mutable:

  • bak w samochodzie – możemy zmienić jego stan wlewając więcej paliwa
  • pióro – stan atramentu ciągle się zmienia
  • telefon – doładowujemy i rozładowujemy baterie

Immutable

  • kolor – raz określony kolor, nie może zostać zmieniony, to co raz określiliśmy jako niebieskie już takie pozostanie
  • biurko – przedmiot codziennego użytku, już raz wyprodukowany, ma określoną szerokość, wysokość, materiał etc.
  • czas – konkretna data, już raz ustalona, jest niezmienialna, tak jak chwila w której piszę ten post jest unikalna

Mam nadzieję, że dzięki przykładom, lepiej czujesz o co chodzi z niezmienialnością obiektów. Zapewne przy nauce programowania obiektowego, cały otaczający Cię świat jest dla Ciebie „zmienialny”, ale po dłuższej chwili namysłu znajdziesz rzeczy, których stanu nie da lub nie powinno się modyfikować.

Jakie benefity daje tworzenie obiektów typu immutable?

Stan zmiennych referencyjnych

Załóżmy, że tworzymy metodę o nazwie ConvertXmlToJson i przekazujemy do niej jakiś obiekt dokumentu XML. Załóżmy taki fragment kodu:

public Json ConvertXMLToJson(XML document) 
{
  //Tutaj dzieje się logika konwersji
}

wygląda niewinnie, jednak typ który przekazujemy jest referencyjny – to znaczy, że jeśli przypadkowo zmodyfikujemy cokolwiek w obiekcie document w metodzie konwersji, to wpływ naszej zmiany będzie widoczny na zewnątrz metody. Być może, będziemy posługiwać się obiektem XML później i co wtedy? Niestety nie będziemy pewni co do stanu obiektu, co może spowodować późniejsze problemy. Gdybyśmy przygotowali klasę XML, tak aby nie można było modyfikować jej stanu problem „niepewności” by zniknął.

Bezpieczeństwo w wielowątkowości

To troszkę bardziej rozbudowany i skomplikowany temat. Zarówno w językach takich jak Java i C# naszą aplikację możemy zrównoleglić za pomocą wątków (część kodu, np. pobieranie pliku zostaje przetworzone przez oddzielną jednostkę obliczeniową w komputerze, dzięki czemu użytkownik ma poczucie, że dana operacja jest wykonywana w tle  – temat wątków jest bardzo obszerny i zastosowałem tutaj bardzo duże uproszczenie, jeśli chcesz wiedzieć więcej to obejrzyj początek tego filmu, w którym tłumacze trochę bardziej o co chodzi: link). W przypadku używania kilku wątków mamy dostęp do zmiennych w obrębie całej aplikacji, wyobraź sobie że w jednym momencie jeden wątek zapisuje jakieś parametry do obiektu, a drugi w tym samym czasie próbuje je odczytać. Jaka będzie wartość odczytanej zmiennej? Nie wiadomo – i takiej sytuacji należy unikać w programowaniu jak ognia. W tym momencie pojawiają się obiekty typu immutable:

Pewność kluczy przy korzystaniu z HashSet i HashMap

Jeśli korzystasz z HashMap lub HashSet-ów to klucze identyfikujące konkretne obiekty zostają utworzone za pomocą hashcode. W momencie kiedy zmodyfikujemy jakieś pole danego obiektu i spróbujemy użyć metody Contains nie znajdziemy obiektu w naszej kolekcji, ponieważ wartość klucza zapisanego w kolekcji jest inna niż wartość hashcode po modyfikacji obiektu. Ten krótki kod pozwoli na przeanalizowanie tej sytuacji:

public class StringHandler{
    private String string;
 
    public StringHandler(String s) {
        this.string = s;
    }
 
    public String getString() {
        return string;
    }
 
    public void setString(String string) {
        this.string = string;
    }
 
    public boolean equals(Object o) {
        if (this == o)
            return true;
        else if (o == null || !(o instanceof StringHandler))
            return false;
        else {
            final StringHendler other = (StringHandler) o;
            if (string == null)
                return (other.string == null);
            else
                return string.equals(other.string);
        }
    }
 
    public int hashCode() {
        return (string != null ? string.hashCode() : 0);
    }
 
    public String toString() {
        return string;
    }
}
   
 public static void main(String[] args) {
    StringHandler sh = new StringHandler("ala");
    HashSet testSet = new HashSet();
    testSet.add(sh);
    sh.setString("ma kota");
    System.out.println(testSet.contains(sh)); // FALSE
    System.out.println(testSet.size()); // 1 
 }
}

Dodajemy obiekt do HashSet, a następnie modyfikujemy jedno z pól obiektu za pomocą settera. Co się dzieje, gdy wywołamy metodę contains? Dostaniemy wartość false, w przypadku obiektów immutable taka sytuacja nie miałaby miejsca 😉

Jak przygotować naszą klasę, aby uniemożliwić zmianę stanu jej obiektów?

Aby zabezpieczyć się przed zmienialnością obiektów pamiętaj:

  • Parametry potrzebne do utworzenia obiektu przekazuj w konstruktorze, najlepiej zastosować tu wzorzec budowniczy: link
  • Kiedy tworzysz nowy obiekt, twórz kopię zmiennych, które mogą zmienić swój stan w trakcie działania programu
  • Postaraj się uniemożliwić modyfikację publicznych pól za pomocą takich słów kluczowych jak readonly lub final
  • Jeśli zwracasz jakieś zmienne typy to należy przekazywać ich kopię np. za pomocą metody Clone, uważaj na shallow copy oraz deep copy
  • Jeśli nie przewidujemy, by można było dziedziczyć po danej klasie, warto ją zapieczętować (uniemożliwić) przed dziedziczeniem, za pomocą takich słów kluczowych jak sealed lub final
  • Najlepiej, gdyby klasa, którą tworzymy dziedziczyła po typie immutable

Obiekty niemutowalne plusy i minusy

Już wiesz czym są obiekty immutable, więc czas na krótkie podsumowanie tej techniki:

Zalety:

  • pewność stanu raz zainicjalizowanych obiektów
  • zmniejszenie problemu z wielowątkowością (wyeliminowanie takich przypadków jak wyścig lub zakleszczenie)
  • redukcja pomyłek podczas operacji na zmiennych referencyjnych
  • poprawne działanie metod typu Contains
  • dokładniejsze odzwierciedlenie świata obiektowego

Wady:

  • dodatkowy narzut pamięciowy oraz czasowy związany z tworzeniem kopii obiektów
  • w przypadku chęci zmodyfikowania pola obiektu, należy utworzyć całkowicie nowy obiekt z zaktualizowanymi wartościami
  • zmiana podejścia do typów niemutowalnych, może zająć chwilę czasu

To wszystko na dziś, dzięki za przeczytanie i powodzenia podczas kodzenia!

 

ZOSTAW ODPOWIEDŹ

Please enter your comment!
Please enter your name here

Loading Facebook Comments ...