Skomplikowane klasy, a zachowanie immutable

Skomplikowane klasy, a zachowanie immutable
teez
  • Rejestracja:ponad 8 lat
  • Ostatnio:ponad 4 lata
  • Postów:122
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.

Kopiuj

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:

Kopiuj
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.
Kopiuj
  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:

Kopiuj
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.

edytowany 6x, ostatnio: teez
teez
@Charles_Ray: co masz na myśli? Problem kompletnie nie tyczy się vavra. Wszystko co wyżej było napisane przy użycia vavr'a można równie dobrze skrobnąć bez niego. To nie jest sedno problemu. :x
Koziołek
Moderator
  • Rejestracja:około 18 lat
  • Ostatnio:25 dni
  • Lokalizacja:Stacktrace
  • Postów:6821
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.


Sięgam tam, gdzie wzrok nie sięga… a tam NullPointerException
teez
Obadam na dniach ten sposób, który proponujesz i dam znać.
Charles_Ray
  • Rejestracja:około 17 lat
  • Ostatnio:3 minuty
  • Postów:1877
2

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


”Engineering is easy. People are hard.” Bill Coughran
Koziołek
Samodzielne pisanie event stora jest całkiem dobrą lekcją, dlaczego tego nie robić.
danek
  • Rejestracja:ponad 10 lat
  • Ostatnio:7 miesięcy
  • Lokalizacja:Poznań
  • Postów:797
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


Spring? Ja tam wole mieć kontrole nad kodem ᕙ(ꔢ)ᕗ
Haste - mała biblioteka do testów z czasem.
edytowany 1x, ostatnio: danek
teez
W momencie gdy wcześniej nie było tego prevState to podczas zakończenia programu mieliśmy w mapie jedynie finalne obiektu typu State. Straciliśmy informację o poprzednich obiektach typu State, która jest potrzebna (z jakiegoś powodu, to jest nie ważne) po zakończeniu programu. I tak jak mówisz - też to zaproponowałem w punkcie 4. Liczyłem na inne propozycję rozwiązań/ jakichś wzorców, bo wydawało mi się, że można to rozwiązać ładniej. :P

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.