Skomplikowane klasy, a zachowanie immutable

0

Załóżmy, że staramy się pisać zgodnie ze sztuką funkcyjnego programowania (na ile pozwala na to Java). Mamy więc same klasy immutable, korzystamy z kolekcji vavr.io. Każda zmiana stanu danej klasy poprzez jakąś metodę powoduje utworzenie nowego obiektu tej klasy. No i teraz mamy taką sytuację.

Mamy klasę State, która częścią czegoś w stylu maszyny stanów.


final class State {

//konstruktor
 private final State nextState;

   public State doSomething(String message) {
        
      // tworzymy nowy obiekt state (kopiując nextState, bo on się nie zmienia) + jakaś zmiana tego stanu
        return new State(message, nextState);
    }
}

stany przechowujemy w HashMapie, która trzyma: ID oraz aktualny stan dla danego ID. Dla ułatwienia niech będzie po prostu tak:

Map<String, State> map = HashMap[...] <== jest to mapa z vavr.io

Dodatkowe założenia:

  1. Mapa ze stanami (oraz "nextState") generowana jest wcześniej.
  2. Istnieje metoda (niżej), która dostaje request i message. Jeżeli znajdzie w mapie State (value w mapie) powiązany z request (key w mapie), wykona na tym State jakąś operację zmieniając jego stan, a następnie zapisuje sobie następnika tego stanu w hashmapie.
  public void exampleMethod(String request, String message) {
 (pomińmy, że tutaj się może coś sypnąć - to nie jest ważne)
        return
                Try.of(() -> map.get(request).get())
                        .andThenTry(state -> state.doSomething(message))
                        .map(State::getNextState)
                        .map(nextState -> map.put(request, nextState))
                        .get();


    }

I załóżmy, że po zakończeniu działania programu mamy w HashMapie takie obiekty typu State, które nie posiadają nextState.

Dochodzi nowa potrzeba: chcemy, po zakończeniu programu, mieć możliwość cofnięcia się z ostatniego stanu (czyli tego w którym aktualnie przebywa w HashMapie pod danym kluczem) do pierwszego. Pierwsza myśl: dodać (analogicznie do nextState) pole prevState i w momencie tworzenia mapy łączyć w listę dwukierunkową. Od teraz nasz State wygląda tak:

final class State {

//konstruktor
 private final State nextState;
 private final State prevState;

   public State doSomething(String message) {
        
      // tworzymy nowy obiekt state (kopiując referencję aktualnego nextState, bo on się nie zmienia) + jakaś zmiana tego stanu
        return new State(message, ????, prevState);
    }
}

**I teraz problem:
**
Metoda doSomething() generuje nowy obiekt typu State (na podstawie obiektu State na którym ta metoda zostala wykonana).
Teraz dochodzimy do sytuacji, że jeżeli mamy listę dwukierunkową z referencjami:

Stan A <=> Stan B <=> C <=> ... => empty.state

I wykonujemy operację na Stanie B tym samym tworząc nowy obiekt Stan B2 musimy:

  1. Stworzyć nowy Stan A2 (którego następnik wskazuje na nowy Stan B2)
  2. Stworzyć nowy Stan C2 (którego poprzednik wskazuje na nowy Stan B2)
  3. Stan B2 wskazuje na nowe stany A2(poprzednik) oraz C2 (następnik)
    I analogicznie musimy przerobić całą listę dwukierunkową (C2 spowoduję zmianę w stanie D, itd..)

Czyli dla potrzeby zmiany jednego pola w klasie State potrzebujemy zrobić od nowa strasznie skomplikowany obiekt.

I teraz możliwe rozwiązania:

  1. Zaciskamy zęby i tworzymy jakąś skomplikowaną metodę, która nam to potworzy.

  2. Delikatnie popuszczamy i wystawiamy setera (doSomething założmy, że finalnie ustawia po prostu jedno pole w klasie więc możemy to zmienić na setSomething(request)).

  3. Wyciągamy listę dwukierunkową z klasy State - jak w takim przypadku najlepiej trzymać index w którym aktualnie stanie się znajdujemy? Możemy wtedy trzymać mapę: Map<String, LinkedList<State>>, ale co właśnie z jakimś indeksem, który nam powie "w tym stanie aktualnie jestem".

  4. Może trzymać drugą strukturę do zapamiętywania i pozbyć się wtedy pola prevState?

@jarekr000000 może coś podpowiesz? :P

Uff.

1

Jest sobie taki wzorzec jak memento, ale będzie tu średnio przydatny. IMO trochę lepszym rozwiązaniem jest kombinacja flyweight i konstrukcji znanych z pcollections. Inaczej mówiąc każda kolejna wersja obiektu, przechowuje tylko deltę w stosunku do pierwszej wersji i referencję do przodka.

2

Możesz przechowywać stan obiektu jako sekwencje niemutowalnych zdarzeń 👉 event sourcing. Nie potrzebujesz do tego żadnych zewnętrznych bibliotek :)

0

Co znaczy

Dochodzi nowa potrzeba: chcemy, po zakończeniu programu, mieć możliwość cofnięcia się z ostatniego stanu

?

Bo może faktycznie starczy trzymać gdzieś tylko zmiany stanu, a w pamięci aplikacji tylko obecny stan

1 użytkowników online, w tym zalogowanych: 0, gości: 1