Jak sensownie zaimplementować maszynę stanów?

Jak sensownie zaimplementować maszynę stanów?
piotrpo
  • Rejestracja:ponad 7 lat
  • Ostatnio:3 dni
  • Postów:3277
0
somekind napisał(a):

No i w przypadku wniosku urlopowego, który ma tylko 4 stany, to pewnie prawda, ale są przypadki, w których zakończenie kolejnego etapu powoduje pojawienie się nowych danych.
Poza tym, to problemem na ogół nie są dane, tylko zarządzanie przechodzeniem po grafie stanów oraz weryfikacja legalności operacji dla danego stanu.
Co do komplikacji, to liczba kombinacji rośnie mocno nieliniowo, dla 20 stanów masz ich 20 razy więcej niż dla 4.

Na początek złożoność maszyny stanów. Jeżeli masz 30 możliwych stanów i 400 możliwych przejść pomiędzy tymi stanami, wynika to z wymagań biznesowych, to tak czy inaczej jakoś tę złożoność musisz zakodować. Możesz to zrobić albo za pomocą 30 klas reprezentujących stany, w których znajdzie się łącznie 400 metod, albo pojedynczej klasy reprezentującej stateful object (np. ten wniosek urlopowy), 30 wartości enuma + 400 definicji [initial state, event, result state]. Kodu mniej - więcej tyle samo.

Maksymalnie prosty diagram stanów jakiegoś wyłącznika:
screenshot-20241220085024.png
Przy tworzeniu klasy per stan miałbyś coś takiego:

Kopiuj
open abstract class Switch(val SwitchId){
  // nie wiemy, czy click będzie w każdym możliwym stanie, nie można wyciągnąć ten metody do klasy nadrzędnej
}

class SwitchOn: Switch{
  clickOff(){
    return SwitchOff()
  }
}

class SwitchOff: Switch{
  clickOn(){
    return SwitchOn()
  }
}

Potrzebujesz do tego jakiejś serializacji i deserializacji:

Kopiuj
fun saveSwitch(switch: Switch){...}
fun findSwitch(id: SwitchId): Switch {...}

I masz pierwszy problem już na poziomie interface'u bazy danych:

Kopiuj
fun userClickedSwith(switchId: SwitchId){
  val switch = findSwitch(switchId)

  if(switch is SwitchOn) switch.clickOff
  if(switch is SwitchOff) switch.clickOff

  saveSwitch(switch)
}

Na warstwie persystencji też nie jest łatwo, bo zakładając tabela na klasę, musisz przeszukać 2 tabele zamiast jednej.

KL
  • Rejestracja:12 miesięcy
  • Ostatnio:około 9 godzin
  • Postów:379
0

@piotrpo: No na poziomie mapowania do bazy trochę zabawy będzie.
Tylko tam @somekind pisał o podejściu TPH, gdzie nie masz oddzielnych tabel na reprezentacje różnych stanów, tylko siedzą one w jednej tabeli z dyskryminatorem typu.

Trochę poruszasz to o czym pisałem wcześniej na przykładzie wniosku urlopowego:

  • wniosek można anulować w stanie Nowy i Przetwarzany (tak jak u ciebie stan przełącznika można zmienić w stanie On i Off)
  • przy akcji anulowania (zmiany stany przełącznika) musisz teraz w taki czy inny sposób pobrać reprezentację odpowiedniego obiektu z bazy
piotrpo
  • Rejestracja:ponad 7 lat
  • Ostatnio:3 dni
  • Postów:3277
0

Jeżeli patrzymy na to na konkretnym przykładzie wniosku urlopowego w dodatku od d.. strony, czyli bazy danych, to najprościej jest użyć jakiejś dokumentowej bazy danych, w której przechowujesz sobie dane tego obiektu (kto wnioskuje, na kiedy), oraz historię eventów zmieniających stan. Jak potrzebujesz ten stan odczytać, to przepuszczasz eventy przez zdefiniowaną FSM i masz stan wynikowy.

Po stronie logiki niby można mieć osobną klasę dla każdego stanu, tylko struktura danych będzie ta sama, a różnić się będą jedynie dostępnymi metodami określającymi możliwe eventy, zwracające nowy obiekt, w stanie wynikowym.

Moim zdaniem takie podejście robi mocno pod górkę, chociaż oczywiście jest obiektowe. Wady tego rozwiązania, to moim zdaniem:
Konieczność sprawdzania typu obiektu i w zależności od wyniku wywoływanie jakiejś metody.
Odczytywanie stanu (np. złożony, zatwierdzony, anulowany) z typu obiektu. Albo będzie tu jakaś metoda stateName():String, albo maper jadący po refleksji.
Cała definicja FSM musi być w kodzie, a wiadomo, że w przypadku procesu biznesowego będzie się zmieniać. Biznes albo zatrudni jakąś nową Anetkę w HR i trzeba będzie wprowadzić nowy stan, żeby miała co robić, albo chociaż zmieni nazwę jakiegoś stanu, żeby sprawiać wrażenie progresu. Przy mapowaniu procesu biznesowego na FSM idealnie by było wyciągnąć FSM z kodu do konfiguracji aplikacji.
Można zrobić to np. tak:

Kopiuj
class State<P>(val payload:P, val fsmDefinition:FSM var state:State){
  fun applyEvent(event Event){
    state = fsmDefinition.stateAfterEvent(state, event)
  }
}

Payload odpowiada za dane, więc mamy czystą implementację FSM, niezależną od danych.

Do tego mamy prostą implementację maszyny stanów:

Kopiuj

class Transition(val entryState:State, event: Event, resultState)

class FSM(val transitions:Transition[]){
  applyuEvent(state: State, event: Event):State{
    transitions.filter(it.entryState == state).filter(it.event = event).map(it.resultState)
  }
}

Oczywiście warto zrobić jakąś walidację samej FSM, czy np. nie ma tam niejednoznacznych/sprzecznych definicji.
Zalety takiego podejścia:
Cała definicja diagramu stanów jest w jednym, no może 3 miejscach (lista stanów, lista zdarzeń, lista dopuszczalnych tranzycji)
Jeżeli chcemy, to State, Event nie muszą być wartościami enum'a, tylko np. Stringiem -> możemy przechowywać te 3 zbiory definiujące FSM w konfiguracji aplikacji, czy tam w bazie danych. Wiadomo, ma to swoje minusy, ale w niektórych zastosowaniach, gdzie proces zmienia się często jest to ogromna zaleta.
Możemy bardzo łatwo zastosować event sourcing, w każdym razie w zakresie FSM, bo wiadomo - payload może się też zmieniać.
W kodzie też moim zdaniem czyściej, bo dziedziczące po sobie klasy danych to zło w czystej postaci.
Na poziomie bazy - jeżeli już się upieramy, że muszą być relację, to tak, payload będzie przechowywany w jednej dużej tabeli, w której część pól będzie null.

KL
  • Rejestracja:12 miesięcy
  • Ostatnio:około 9 godzin
  • Postów:379
0
piotrpo napisał(a):

Jeżeli patrzymy na to na konkretnym przykładzie wniosku urlopowego w dodatku od d.. strony, czyli bazy danych, to najprościej jest użyć jakiejś dokumentowej bazy danych, w której przechowujesz sobie dane tego obiektu (kto wnioskuje, na kiedy), oraz historię eventów zmieniających stan. Jak potrzebujesz ten stan odczytać, to przepuszczasz eventy przez zdefiniowaną FSM i masz stan wynikowy.

Czemu od d**y strony?

Cały wątek zanim go reaktywowałem skończył się na tym, że albo definiujemy maszynę stanów jako oddzielne obiekty reprezentujące stany mające tylko dostępne metody, albo jako typ wyliczeniowy ze słownikiem przejść.

Więc jak już mamy ustalone z grubsza możliwości implementacji to się po prostu zastanawiałem jak teraz to przechowywać w bazce w obu podejściach.

A najprościej jak dla mnie to jednak typ wyliczeniowy nawet bez event sourcingu. Jak trzeba historię przechować to też można to ogarnąć prostym wpisem audytowym do bazki przy zmienianiu stanu.
Nie mówię że najlepiej, tylko najprościej.

Moim zdaniem takie podejście robi mocno pod górkę, chociaż oczywiście jest obiektowe. Wady tego rozwiązania, to moim zdaniem:
Konieczność sprawdzania typu obiektu i w zależności od wyniku wywoływanie jakiejś metody.

Ale przecież ze 3 czy 4 komentarze wyżej pośrednio do tego nawiązywałem i doszliśmy, że nie jest to w ogólności potrzebne.
Jak wywołuję cancel to pobieram z jakiegoś repo obiekt DokumentDoAnulowania.
Jak takiego obiektu repo nie zwróci, bo w bazce nie ma rekordu Dokument ze stanem Nowy albo Przetwarzany to znaczy że nie da się anulować. Jak dostałem z repo obiekt DokumentDoAnulowania to znaczy że mogę na nim wywołać akcję Anuluj, która coś tam poweryfikuje i albo anuluje albo nie.

I o ile zgadzam się, że to się też pokomplikuje jak daną akcję można wykonywać w kilku stanach (sama logika wyciągania danych i może później z powrotem zmapowanie tego na model bazodanowy), to wciąż konieczności sprawdzania typu tutaj nie ma.

Odczytywanie stanu (np. złożony, zatwierdzony, anulowany) z typu obiektu. Albo będzie tu jakaś metoda stateName():String, albo maper jadący po refleksji.

Z podejściem opisanym wyżej to chyba nie jest potrzebne?

Cała definicja FSM musi być w kodzie, a wiadomo, że w przypadku procesu biznesowego będzie się zmieniać. Biznes albo zatrudni jakąś nową Anetkę w HR i trzeba będzie wprowadzić nowy stan, żeby miała co robić, albo chociaż zmieni nazwę jakiegoś stanu, żeby sprawiać wrażenie progresu. Przy mapowaniu procesu biznesowego na FSM idealnie by było wyciągnąć FSM z kodu do konfiguracji aplikacji.

Jak ktoś buduje konfigurowalny silnik do obsługi procesów biznesowych to pewnie tak, lepiej to wydzielić do konfiguracji czy zaciągać definicję procesu z zewnątrz. Wtedy podejście z klasą per stan się nie sprawdzi.
Ale nie przesadzałbym że wszędzie wymagane jest budowanie własnego silnika ;)
Czasami chcesz zamodelować kilka procesów na krzyż i zmiana w kodzie w przypadku zmiany procesu nie będzie wcale taka zła.

Zalety takiego podejścia:
Cała definicja diagramu stanów jest w jednym, no może 3 miejscach (lista stanów, lista zdarzeń, lista dopuszczalnych tranzycji)
Jeżeli chcemy, to State, Event nie muszą być wartościami enum'a, tylko np. Stringiem -> możemy przechowywać te 3 zbiory definiujące FSM w konfiguracji aplikacji, czy tam w bazie danych. Wiadomo, ma to swoje minusy, ale w niektórych zastosowaniach, gdzie proces zmienia się często jest to ogromna zaleta.
Możemy bardzo łatwo zastosować event sourcing, w każdym razie w zakresie FSM, bo wiadomo - payload może się też zmieniać.
W kodzie też moim zdaniem czyściej, bo dziedziczące po sobie klasy danych to zło w czystej postaci.
Na poziomie bazy - jeżeli już się upieramy, że muszą być relację, to tak, payload będzie przechowywany w jednej dużej tabeli, w której część pól będzie null.

Spoko, widzę w tym zalety i nie mówię, że zawsze trzeba robić albo tak, albo tak.

Co prawda nie wiem o jakim dziedziczeniu klas danych tutaj mówisz, bo chyba w opisanym podejściu z typem per stan nie jest to wcale potrzebne. Po prostu klasa reprezentująca dany stan udostępnia dane przydatne w tym stanie.

edytowany 1x, ostatnio: Klaun
piotrpo
  • Rejestracja:ponad 7 lat
  • Ostatnio:3 dni
  • Postów:3277
0
Klaun napisał(a):
piotrpo napisał(a):

Jeżeli patrzymy na to na konkretnym przykładzie wniosku urlopowego w dodatku od d.. strony, czyli bazy danych, to najprościej jest użyć jakiejś dokumentowej bazy danych, w której przechowujesz sobie dane tego obiektu (kto wnioskuje, na kiedy), oraz historię eventów zmieniających stan. Jak potrzebujesz ten stan odczytać, to przepuszczasz eventy przez zdefiniowaną FSM i masz stan wynikowy.

Czemu od d**y strony?
Cały wątek zanim go reaktywowałem skończył się na tym, że albo definiujemy maszynę stanów jako oddzielne obiekty reprezentujące stany mające tylko dostępne metody, albo jako typ wyliczeniowy ze słownikiem przejść.

Więc jak już mamy ustalone z grubsza możliwości implementacji to się po prostu zastanawiałem jak teraz to przechowywać w bazce w obu podejściach.

W drugim przypadku jako typ wyliczany (czyli jakiś varchar w RDBMS). Pierwszy sposób z wieloma typami - nie mam pojęcia.

A najprościej jak dla mnie to jednak typ wyliczeniowy nawet bez event sourcingu. Jak trzeba historię przechować to też można to ogarnąć prostym wpisem audytowym do bazki przy zmienianiu stanu.
Nie mówię że najlepiej, tylko najprościej.

Czy najlepiej, to definiuje problem biznesowy. Jak nie potrzebujesz historii, to nie koniecznie jest sens ją trzymać. Chyba, że są względy techniczne (np. asychronicznie wpadające eventy)

Jak wywołuję cancel to pobieram z jakiegoś repo obiekt DokumentDoAnulowania.
Jak takiego obiektu repo nie zwróci, bo w bazce nie ma rekordu Dokument ze stanem Nowy albo Przetwarzany to znaczy że nie da się anulować. Jak dostałem z repo obiekt DokumentDoAnulowania to znaczy że mogę na nim wywołać akcję Anuluj, która coś tam poweryfikuje i albo anuluje albo nie.

Idea jest taka, że na każdym stanie możesz wykonać każdą akcję. Zwyczajnie tam gdzie jest ona nielegalna nic się nie stanie. Jak stare spłuczki w kibelkach. Możesz sobie ciągnąć za sznurek dowoli, ale woda poleci dopiero jak rezerwuar się napełni.

I o ile zgadzam się, że to się też pokomplikuje jak daną akcję można wykonywać w kilku stanach (sama logika wyciągania danych i może później z powrotem zmapowanie tego na model bazodanowy), to wciąż konieczności sprawdzania typu tutaj nie ma.

Pobierasz wszystkie wnioski urlopowe z bazy. Masz kolekcję obiektów różnych typów, Zanim wywołasz na jakimś metodę cancel, musisz wiedzieć, że ta metoda tam jest.

Co prawda nie wiem o jakim dziedziczeniu klas danych tutaj mówisz, bo chyba w opisanym podejściu z typem per stan nie jest to wcale potrzebne. Po prostu klasa reprezentująca dany stan udostępnia dane przydatne w tym stanie.

Jeżeli masz podejście z klasami, to ja widzę je jako:

Kopiuj
Wniosek()
WniosekZłożony():Wniosek
WniosekZatwierdzony():Wniosek
...
Miang
  • Rejestracja:prawie 7 lat
  • Ostatnio:około godziny
  • Postów:1659
1

Wy robicie ćwiczenie do szkoły z maszyny stanów, czy próbujecie ją implementować bo się wydaje dobrym rozwiązaniem przy znanych założeniach danego problemu biznesowego?
Bo sorry ale te rozważania wyglądają jakby byście mówili o ćwiczeniu typu sztuka dla sztuki

piotrpo napisał(a):

Jeżeli patrzymy na to na konkretnym przykładzie wniosku urlopowego w dodatku od d.. strony, czyli bazy danych, to najprościej jest użyć jakiejś dokumentowej bazy danych, w której przechowujesz sobie dane tego obiektu (kto wnioskuje, na kiedy), oraz historię eventów zmieniających stan. Jak potrzebujesz ten stan odczytać, to przepuszczasz eventy przez zdefiniowaną FSM i masz stan wynikowy.

proszenie się o kłopoty
w ogóle pytanie czy te eventy są potrzebne zawsze czy w jakimś wyjątkowym przypadku gdy potrzebuje znaleźć historię, a tak normalnie zazwyczaj bierzesz pod uwagę: nagłówek dokumentu, stan

Po stronie logiki niby można mieć osobną klasę dla każdego stanu, tylko struktura danych będzie ta sama, a różnić się będą jedynie dostępnymi metodami określającymi możliwe eventy, zwracające nowy obiekt, w stanie wynikowym.

dziedziczenie. metody czysto wirtualne (mówię o c++).

Moim zdaniem takie podejście robi mocno pod górkę, chociaż oczywiście jest obiektowe. Wady tego rozwiązania, to moim zdaniem:

a obiektowe musi być bo? mówię że robicie se ćwiczenie ;)

Konieczność sprawdzania typu obiektu i w zależności od wyniku wywoływanie jakiejś metody.
Odczytywanie stanu (np. złożony, zatwierdzony, anulowany) z typu obiektu. Albo będzie tu jakaś metoda stateName():String, albo maper jadący po refleksji.
Cała definicja FSM musi być w kodzie, a wiadomo, że w przypadku procesu biznesowego będzie się zmieniać. Biznes albo zatrudni jakąś nową Anetkę w HR i trzeba będzie wprowadzić nowy stan, żeby miała co robić, albo chociaż zmieni nazwę jakiegoś stanu, żeby sprawiać wrażenie progresu. Przy mapowaniu procesu biznesowego na FSM idealnie by było wyciągnąć FSM z kodu do konfiguracji aplikacji.

jprd lepsze niż pomysły dyrektorka z EAV. Spoko Anetka zwaliduje czy nowy diagram przejścia ma sens i przyjmie na klatę to że rozpieprzyła cały proces i działanie systemu przez swój błąd. Co może pójść nie tak?
A istniejące dokumenty skoro będą w historii miały nielegalne przejścia najlepiej wywalić automatycznie (ironia)


dzisiaj programiści uwielbiają przepisywać kod z jednego języka do drugiego, tylko po to by z projektem nadal stać w miejscu ale na nowej technologii
edytowany 1x, ostatnio: Miang
piotrpo
  • Rejestracja:ponad 7 lat
  • Ostatnio:3 dni
  • Postów:3277
0

@Miang Nie eventy nie są potrzebne. Chyba, że implementujesz FSM, to wtedy bez nich ciężko.
Metody wirtualne - jaka to niby ma być metoda wirtualna skoro z założenia klasy dziedziczące implementują różne zestawy metod?
Obiektowe nie musi być. Moim zdaniem nie powinno.
Anetka z HR nie zwaliduje diagramu stanów. Ale zdecydowanie wpadnie na pomysł, że coś tam trzeba jeszcze dodać do procesu.

KL
  • Rejestracja:12 miesięcy
  • Ostatnio:około 9 godzin
  • Postów:379
0
piotrpo napisał(a):

Idea jest taka, że na każdym stanie możesz wykonać każdą akcję. Zwyczajnie tam gdzie jest ona nielegalna nic się nie stanie. Jak stare spłuczki w kibelkach. Możesz sobie ciągnąć za sznurek dowoli, ale woda poleci dopiero jak rezerwuar się napełni.

W sumie to nie wiem czy taka jest idea.
Jak dla mnie koncepcyjnie w danym stanie powinna być możliwa do wykonania tylko akcja na którą stan zezwala.

Czy to zrealizujemy przez system typów czy walidacje na jednym typie to trochę sprawa wtórna.

Pobierasz wszystkie wnioski urlopowe z bazy. Masz kolekcję obiektów różnych typów, Zanim wywołasz na jakimś metodę cancel, musisz wiedzieć, że ta metoda tam jest.

A dlaczego w warstwie dostępu do danych nie pobrać wniosku o danym id do jakiejkolwiek struktury, a później na jej podstawie albo stworzyć DokumentDoAnulowania, jeśli wniosek jest w stanie dozwolonym do anulowania albo zwrócić pusty rezultat (co by oznaczało że nie da się anulować danego wniosku)?

Nie trzeba wtedy sprawdzać typu obiektu.

Jeżeli masz podejście z klasami, to ja widzę je jako:

Wniosek()
WniosekZłożony():Wniosek
WniosekZatwierdzony():Wniosek
...

Tak może być, ale nie musi.
Przecież mogę mieć niezależne definicje typów gdzie każda udostępnia tylko dane mające sens w danym stanie i nie muszę tu mieć żadnego dziedziczenia.

Miang
  • Rejestracja:prawie 7 lat
  • Ostatnio:około godziny
  • Postów:1659
0
Miang napisał(a):

Po stronie logiki niby można mieć osobną klasę dla każdego stanu, tylko struktura danych będzie ta sama, a różnić się będą jedynie dostępnymi metodami określającymi możliwe eventy, zwracające nowy obiekt, w stanie wynikowym.

dziedziczenie. metody czysto wirtualne (mówię o c++).

no dobra, tu się pomyliłam, czysto wirtualne nie, tylko wirtualne z implementacja w klasie podstawowej co wyjątek rzucają, a nadpisane jedynie w tych pochodnych gdzie maja być dostępne


dzisiaj programiści uwielbiają przepisywać kod z jednego języka do drugiego, tylko po to by z projektem nadal stać w miejscu ale na nowej technologii
edytowany 1x, ostatnio: Miang
piotrpo
  • Rejestracja:ponad 7 lat
  • Ostatnio:3 dni
  • Postów:3277
0

W sumie to nie wiem czy taka jest idea.
Jak dla mnie koncepcyjnie w danym stanie powinna być możliwa do wykonania tylko akcja na którą stan zezwala.

Nikt ci nie zabrania zaimplementowania tego na poziomie walidacji. Prawdopodobnie użytkownik będzie zadowolony, że nie klika guzika "zatwierdź", który nic nie zmienia. Koncepcja polega na tym, że masz jakiś obiekt z dyskretnym stanem, jeżeli stanie się coś z zamkniętego katalogu zdarzeń, to ten stan zostanie zmieniony na wskazany. W każdym innym wypadku stan nie ulegnie modyfikacji.

Nie trzeba wtedy sprawdzać typu obiektu.

Masz metodę dostępu do danych:

Kopiuj
fun findWniosek(id: Int):Wniosek

Po jej wywołaniu dostaniesz wniosek z jakimś stanem. Funkcja może zwrócić dowolny typ z dziedziczących. Żeby wywołać na nim jakąś metodę (zdarzenie), musisz wiedzieć, że ta metoda tam jest, czyli musisz sprawdzić typ szczegółowy, rzutować na niego to co zwróciła funkcja.
Alternatywą jest, że wewnątrz Wniosek masz jakieś pole określające stan i np. zapakujesz go w jakiś wrapper.
Tak czy inaczej, dostajesz coś, musisz sprawdzić co to jest i dopiero wykonać, lub nie jakąś metodę.

Czyli:

A dlaczego w warstwie dostępu do danych nie pobrać wniosku o danym id do jakiejkolwiek struktury, a później na jej podstawie albo stworzyć DokumentDoAnulowania, jeśli wniosek jest w stanie dozwolonym do anulowania albo zwrócić pusty rezultat (co by oznaczało że nie da się anulować danego wniosku)?

Bo pobierając wniosek o zadanym id nie wiesz w jakim on jest stanie.

KL
  • Rejestracja:12 miesięcy
  • Ostatnio:około 9 godzin
  • Postów:379
1
piotrpo napisał(a):

Nikt ci nie zabrania zaimplementowania tego na poziomie walidacji. Prawdopodobnie użytkownik będzie zadowolony, że nie klika guzika "zatwierdź", który nic nie zmienia. Koncepcja polega na tym, że masz jakiś obiekt z dyskretnym stanem, jeżeli stanie się coś z zamkniętego katalogu zdarzeń, to ten stan zostanie zmieniony na wskazany. W każdym innym wypadku stan nie ulegnie modyfikacji.

No przecież o tym piszę.
Koncepcja jest taka że w danych stanach można wykonać dane akcje, a innych nie
To jak to zostanie zaimplementowane nie ma znaczenia w ogólności. Ma w naszych rozważaniach bo o tym jest temat.

Wcześniej napisałeś Idea jest taka, że na każdym stanie możesz wykonać każdą akcję co zrozumiałem że przedstawiasz to jako ogólną ideę maszyny stanów.
A to tylko jeden z wielu sposobów implementacji.

Nie trzeba wtedy sprawdzać typu obiektu.

Masz metodę dostępu do danych:

Kopiuj
fun findWniosek(id: Int):Wniosek

Po jej wywołaniu dostaniesz wniosek z jakimś stanem. Funkcja może zwrócić dowolny typ z dziedziczących. Żeby wywołać na nim jakąś metodę (zdarzenie), musisz wiedzieć, że ta metoda tam jest, czyli musisz sprawdzić typ szczegółowy, rzutować na niego to co zwróciła funkcja.
Alternatywą jest, że wewnątrz Wniosek masz jakieś pole określające stan i np. zapakujesz go w jakiś wrapper.
Tak czy inaczej, dostajesz coś, musisz sprawdzić co to jest i dopiero wykonać, lub nie jakąś metodę.

Czyli:

A dlaczego w warstwie dostępu do danych nie pobrać wniosku o danym id do jakiejkolwiek struktury, a później na jej podstawie albo stworzyć DokumentDoAnulowania, jeśli wniosek jest w stanie dozwolonym do anulowania albo zwrócić pusty rezultat (co by oznaczało że nie da się anulować danego wniosku)?

Bo pobierając wniosek o zadanym id nie wiesz w jakim on jest stanie.

Zakładasz że warstwa aplikacyjna/domenowa bazuje na ogólnym typie Wniosek co wymaga później sprawdzania typów w runtime.

Przecież warstwa dostępu do danych może zwrócić typ specyficzny dla kontekstu operacji.

Wtedy zamiast fun findWniosek(id: Int):Wniosek mogę mieć fun findWniosekDoAnulowania(id: Int):WniosekDoAnulowania.

I jakaś implementacja (pseudokod):

Kopiuj
fun findWniosekDoAnulowania(id: Int):WniosekDoAnulowania` {
   val wniosekPersistenceModel = db.wnioski.where(it.id == id).where(it.status==nowy || it.status == przetwarzany).firstOrDefault();
   if (wniosekPersistenceModel is null) return null;

    return new WniosekDoAnulowania(wniosekPersistenceModel);
}

Nie widzę tutaj rzutowania czy innego sprawdzania typu w runtime.
Przy okazji doszliśmy do niezależności modelu domeny od modelu persystencji.

edytowany 1x, ostatnio: Klaun
Miang
  • Rejestracja:prawie 7 lat
  • Ostatnio:około godziny
  • Postów:1659
0

albo funkcja ZamianStanu(enum_stan nowy_stan) która wywoła funkcje prywatne zmieniające stan i zwróci true lub zwróci false


dzisiaj programiści uwielbiają przepisywać kod z jednego języka do drugiego, tylko po to by z projektem nadal stać w miejscu ale na nowej technologii
piotrpo
  • Rejestracja:ponad 7 lat
  • Ostatnio:3 dni
  • Postów:3277
1

Zakładasz że warstwa aplikacyjna/domenowa bazuje na ogólnym typie Wniosek co wymaga później sprawdzania typów w runtime.

No prosty use case - manager wyświetla sobie listę wszystkich wniosków podległych mu pracowników. Te wnioski są w różnych stanach. Część jest złożonych, część zatwierdzonych itd. Klika jakiś wniosek do zatwierdzenia. Pobieranie jeszcze raz tego wniosku z bazy, ale tym razem jako klasę specyficzną dla jego stanu jest moim zdaniem mocno bez sensu.
Jeżeli chcesz mapować dane na klasę z metodami odpowiadającymi

Tak systematyzując te nasze poplątane dyskusje.
FSM jest dość dokładnie opisaną koncepcją. Nie ma obowiązku jej używania, nie zawsze warto jej używać, ale czasami się przydaje, bo pozwala formalnie opisać jakiś problem. Im bliżej implementacja będzie tego formalnego opisu, tym lepiej, bo mniej okazji do pomyłek, a jak jakaś się trafi to łatwiej ją znaleźć i poprawić.

Formalnie, FSM jest grafem, w którym węzłami są stany, przejścia są wyzwalane na podstawie akcji. Zupełnie tak jak w pierwszej klasie podstawówki:
screenshot-20241220204231.png

Do zapisania takiego stanu (co było twoim pytaniem) - moim zdaniem wszystko jedno. Można wejść na poziom bazy danych, tego jakie sposoby będą bardziej, a jakie mniej wydajne, ale uważam, że to mało istotne szczegóły.

Po stronie kodu można ten mechanizm implementować rozmaicie. Jednym ze sposobów jest użycie różnych (pod)typów dla rozróżnienia stanu. Jeżeli stan jest przechowywany w programie jako typ obiektu, to żeby go odczytać, trzeba odczytać ten typ.
Tutaj wchodzą moje fobie z Java. Niektóre inne języki mają to rozwiązane lepiej, ale w Java taki kod:

Kopiuj
PrivateType object = findObject(.....)
if(object instance of WniosekDoZatwierdzenia){
  WniosekDoZatwierdzenia wniosek = (WniosekDoZatwierdzenia) object;
  wniosek.zatwierdz();
}

Podpali kompilator w linii z rzutowaniem i zacznie latać uchnecked cast warning. Może nie tragedia, ale nie lubię.

Załóżmy, że chcesz dodać menu kontekstowe z możliwymi akcjami. W jaki sposób to zrobisz? Refleksja i sprawdzanie czy jakaś metoda istnieje? Można, ale sporo energii psu w d....ę.

Teraz "mój" sposób, czyli tworzymy sobie listy stanów, zdarzeń, tranzycji. Można to zakodować 1:1 zgodnie z diagramem w parę minut. Tworzymy na tej podstawie jakąś instancję FSM. Można ją walidować (czy nie ma osieroconych stanów, stanów bez wyjścia, niejednoznacznych tranzycji itd.). Rzeczywisty problem tutaj, to walidacja w runtime a nie compile time. Zgadzam się, traktuję to jako cenę. Z drugiej strony linijka testu wywołująca tę walidację rozwiązuje problem runtime, czyli nie jest tak tragicznie.

Natomiast później można sobie w tej instancji FSM zaimplementować takie cuda:
fsm.getLegalActionsForState(State state) i wiedzieć które guziki na UI zapalić
fsm.getResultState(initialState, action) - jak działąnie jest zdefiniowane, to dostajesz wynik, jak nie jest zdefiniowane, to go nie dostajesz.

Masz, a w każdym razie możesz mieć prawie całkowitą separację danych od całego tego cyrku z FSM. Prawie, bo jednak wartość stanu gdzieś trzeba przechować.
Możesz jeden "silnik" stosować z różnymi definicjami FSM. Czyli kilkadziesiąt linijek kodu za pierwszym razem, za drugim składasz 3 listy/enumy.

KL
  • Rejestracja:12 miesięcy
  • Ostatnio:około 9 godzin
  • Postów:379
1

No i spoko.
Ja tylko pisałem że można mieć typ per stan i to w samo w sobie nie oznacza jeszcze konieczności rzutowania czy refleksji.

Co w jakim scenariuszu będzie lepsze zależy od sytuacji i nie ma co popadać w paranoję.

No prosty use case - manager wyświetla sobie listę wszystkich wniosków podległych mu pracowników. Te wnioski są w różnych stanach. Część jest złożonych, część zatwierdzonych itd. Klika jakiś wniosek do zatwierdzenia. Pobieranie jeszcze raz tego wniosku z bazy, ale tym razem jako klasę specyficzną dla jego stanu jest moim zdaniem mocno bez sensu.

W apce webowej to dokładnie tak bym zrobił.
Do wyświetlania by poleciał view model odczytany jakimś na tyle na ile to możliwe prostym SQLem i zmapowany bezpośrednio pomijając potencjalną złożoność modelu do zapisu.
Przy zapisie bym to przepychał przez modele domenowe/agregaty. Tak to się chyba z grubsza robi w CQRS?

piotrpo
  • Rejestracja:ponad 7 lat
  • Ostatnio:3 dni
  • Postów:3277
0

CQRS to znowu zupełnie inny temat (albo nie potrafię znaleźć związku). Zakładając, że użycie tego wzorca jest zasadne, robi się to najczęściej po to, żeby odseparować manipulację danymi od ich odczytu (truizm), w celu nieograniczonego skalowania odczytu.
Oczywiście tak jak wspominałem wyżej, można te eventy strumieniować i rozgłaszać do iluś tam innych mikroserwisów, które na ich podstawie będą sobie lokalnie aktualizować stan. W praktyce dochodzi do tego jeszcze piminięta w tym wątku część danych (kto złożył wniosek, kto coś w nim uzupełnił), która też musiałaby być rozgłaszana.

KL
CQRS nawiązywał tylko do tego Pobieranie jeszcze raz tego wniosku z bazy, ale tym razem jako klasę specyficzną dla jego stanu jest moim zdaniem mocno bez sensu.. Bo to zabrzmiało jakby oddzielny model do wyświetlenia i oddzielny model do zapisu był czymś bez sensu, a to przecież standard w aplikacjach z bardziej skomplikowaną domeną.
LukeJL
  • Rejestracja:około 11 lat
  • Ostatnio:około 7 godzin
  • Postów:8406
1

A co wy budujecie? Jakiś taki praktyczny diagram by się przydał, jakie problemy ta maszyna stanów miałaby rozwiązać.
Np. ten DokumentAnulowany DokumentZatwierdzony itp. z czym to się łączy później? Czy to jest lokalny stan dokumentu, czy ma wpływ na resztę?
Ja szczerze mówiąc nie bardzo rozumiem już, czego ta dyskusja dotyczy (pod kątem jakiegoś przykładu/potrzeby).

Na początek złożoność maszyny stanów. Jeżeli masz 30 możliwych stanów i 400 możliwych przejść pomiędzy tymi stanami, wynika to z wymagań biznesowych

Albo nie wynika z wymagań biznesowych, a ze zwykłej duplikacji kodu czy ze złej abstrakcji.


edytowany 2x, ostatnio: LukeJL
Miang
no overengineering tworzą ;)
KL
Czy to jest lokalny stan dokumentu W moim wyobrażeniu lokalny stan dokumentu, który jest procesowany.
KL
A co wy budujecie? Jakiś taki praktyczny diagram by się przydał, jakie problemy ta maszyna stanów miałaby rozwiązać. Żaden konkretny. Z mojej strony to raczej po prostu teoretyczne rozważania o sposobach implementacji "biznesowej" maszyny stanów. Czyli reprezentującej biznesowy proces w jakiejś typowej korpoapce, a nie techniczny sposób na implementację niskopoziomowej maszyny stanów (do czego nawiązywały początkowe odpowiedzi w tym temacie).
LukeJL
@Miang no też mi się tak wydaje. Ew. niedoinżynierowanie (chodzi o to, że ta dyskusja jest jakoś dziwnie zafiksowana na tym, żeby zrobić koniecznie FSM, taki ze stanami i tablicą przejść, nawet jeśli to może być zbyt naiwne rozwiązanie na rzeczywiste potrzeby).
LukeJL
@Klaun no ja właśnie nie widzę w postach w tej dyskusji konkretnych problemów biznesowych, tylko coś w stylu a bo że ładujemy dokumenty, i że są różne stany tych dokumentów, rozumisz albo 400 przejść, kurde. To dużo czy mało? czy pisanie o klasach/szczegółach implementacji, które nie wiadomo, co mają robić.
Miang
  • Rejestracja:prawie 7 lat
  • Ostatnio:około godziny
  • Postów:1659
1
piotrpo napisał(a):

Zakładasz że warstwa aplikacyjna/domenowa bazuje na ogólnym typie Wniosek co wymaga później sprawdzania typów w runtime.

No prosty use case - manager wyświetla sobie listę wszystkich wniosków podległych mu pracowników. Te wnioski są w różnych stanach. Część jest złożonych, część zatwierdzonych itd. Klika jakiś wniosek do zatwierdzenia. Pobieranie jeszcze raz tego wniosku z bazy, ale tym razem jako klasę specyficzną dla jego stanu jest moim zdaniem mocno bez sensu.

do pokazania listy powinna na front lecieć lista , taka tradycyjna select nawa stan from tabela a nie obiekty


dzisiaj programiści uwielbiają przepisywać kod z jednego języka do drugiego, tylko po to by z projektem nadal stać w miejscu ale na nowej technologii
edytowany 1x, ostatnio: Miang
somekind
Moderator
  • Rejestracja:około 17 lat
  • Ostatnio:około 6 godzin
  • Lokalizacja:Wrocław
1
piotrpo napisał(a):

Na początek złożoność maszyny stanów. Jeżeli masz 30 możliwych stanów i 400 możliwych przejść pomiędzy tymi stanami, wynika to z wymagań biznesowych, to tak czy inaczej jakoś tę złożoność musisz zakodować. Możesz to zrobić albo za pomocą 30 klas reprezentujących stany, w których znajdzie się łącznie 400 metod, albo pojedynczej klasy reprezentującej stateful object (np. ten wniosek urlopowy), 30 wartości enuma + 400 definicji [initial state, event, result state]. Kodu mniej - więcej tyle samo.

Absolutnie nie.
400 metod będzie w jednej klasie zamiast w 30 (ergo klasa będzie miała pewnie tysiące linii kodu), różnica będzie taka, że zamiast zwracać obiekt stanu, będą zwracały void i ustawiały stan wewnętrzny obiektu.
Do tego każda z tych metod będzie musiała najpierw sprawdzić, czy będzie mogła być wywołana w danym stanie obiektu. Nawet jeśli to będzie tylko jeden if, to będzie to 400 zbędnych ifów.

Maksymalnie prosty diagram stanów jakiegoś wyłącznika:

Już pisałem, przy prostych sytuacjach, to nie ma znaczenia - za to rozrasta się kosmicznie w miarę przybywania stanów.

Na warstwie persystencji też nie jest łatwo, bo zakładając tabela na klasę, musisz przeszukać 2 tabele zamiast jednej.

Po pierwsze nie muszę mieć żadnych tabel, po drugie bez problemu mogę trzymać dane w jednej tabeli.
Jeśli kotlinowe ormy są tak upośledzone, że nie potrafią mapować, to trzeba wybrać inny język.

piotrpo napisał(a):

Wady tego rozwiązania, to moim zdaniem:
Konieczność sprawdzania typu obiektu i w zależności od wyniku wywoływanie jakiejś metody.
Odczytywanie stanu (np. złożony, zatwierdzony, anulowany) z typu obiektu. Albo będzie tu jakaś metoda stateName():String, albo maper jadący po refleksji.

Wystarczy wczytać stan z bazy, i utworzyć obiekt reprezentujący konkretny stan za pomocą metody z danej klasy.

Cała definicja FSM musi być w kodzie, a wiadomo, że w przypadku procesu biznesowego będzie się zmieniać. Biznes albo zatrudni jakąś nową Anetkę w HR i trzeba będzie wprowadzić nowy stan, żeby miała co robić, albo chociaż zmieni nazwę jakiegoś stanu, żeby sprawiać wrażenie progresu. Przy mapowaniu procesu biznesowego na FSM idealnie by było wyciągnąć FSM z kodu do konfiguracji aplikacji.

No będzie, albo nie będzie. Jeśli zakładasz, że będzie, to w ogóle trzeba użyć jakiegoś istniejącego rozwiązania do workflowów, a nie bawić się samemu.
O budowie własnego frameworka do workflowów nie było póki co mowy w wątku.

Cała definicja diagramu stanów jest w jednym, no może 3 miejscach (lista stanów, lista zdarzeń, lista dopuszczalnych tranzycji)

No i masz ogromną konfigurację gdzieś tam, a jak patrzysz w kod klasy, to nie wiesz, co po czym może nastąpić.

piotrpo napisał(a):

Idea jest taka, że na każdym stanie możesz wykonać każdą akcję. Zwyczajnie tam gdzie jest ona nielegalna nic się nie stanie.

Tylko, żeby nic się nie stało, najpierw trzeba włożyć wysiłek w zaimplementowanie walidacji stanu, i zabezpieczenie przed tym, żeby aplikacja się wywaliła.
To, że nic się nie stanie, nie jest za darmo.

piotrpo napisał(a):

Bo pobierając wniosek o zadanym id nie wiesz w jakim on jest stanie.

Oczywiście, że wiem, przecież to jest zapisane w bazie.

piotrpo napisał(a):

No prosty use case - manager wyświetla sobie listę wszystkich wniosków podległych mu pracowników. Te wnioski są w różnych stanach. Część jest złożonych, część zatwierdzonych itd. Klika jakiś wniosek do zatwierdzenia. Pobieranie jeszcze raz tego wniosku z bazy, ale tym razem jako klasę specyficzną dla jego stanu jest moim zdaniem mocno bez sensu.

W czym problem?
Klasa modelu służącego do wyświetlenia danych w liście dla managera, będzie miała wyliczane na podstawie stanu propertisy czy tam metody typu: CanBeCanceled, CanBeApproved, itd.

Po stronie kodu można ten mechanizm implementować rozmaicie. Jednym ze sposobów jest użycie różnych (pod)typów dla rozróżnienia stanu. Jeżeli stan jest przechowywany w programie jako typ obiektu, to żeby go odczytać, trzeba odczytać ten typ.
Tutaj wchodzą moje fobie z Java. Niektóre inne języki mają to rozwiązane lepiej, ale w Java taki kod:

Kopiuj
PrivateType object = findObject(.....)
if(object instance of WniosekDoZatwierdzenia){
  WniosekDoZatwierdzenia wniosek = (WniosekDoZatwierdzenia) object;
  wniosek.zatwierdz();
}

Podpali kompilator w linii z rzutowaniem i zacznie latać uchnecked cast warning. Może nie tragedia, ale nie lubię.

Z całym szacunkiem, ale ten kod, to jakieś badziewie jest.
Jedynym momentem, w którym trzeba sprawdzać typ obiektu jest moment zapisu - żeby wiedzieć jak ustawić w rekordzie/dokumencie bazodanowym wartość stanu dokumentu.
Po wczytaniu z warstwy danych obiekt ma te metody, które ma, i innych nie można na nim wywołać. Kompilator jest szczęśliwy (i programista też).

Załóżmy, że chcesz dodać menu kontekstowe z możliwymi akcjami. W jaki sposób to zrobisz? Refleksja i sprawdzanie czy jakaś metoda istnieje? Można, ale sporo energii psu w d....ę.

Ale co ma menu kontekstowe do logiki przepływu? Obiekty w menu kontekstowym, to nie są obiekty logiki biznesowej.

Z drugiej strony linijka testu wywołująca tę walidację rozwiązuje problem runtime, czyli nie jest tak tragicznie.

Jest tragicznie, bo rośnie niemalże wykładniczo, i po jakimś czasie nikt z godnością nie chce takiego badziewia utrzymywać.

piotrpo
  • Rejestracja:ponad 7 lat
  • Ostatnio:3 dni
  • Postów:3277
0

400 metod będzie w jednej klasie zamiast w 30

Jakie 400 metod w jednej klasie? Jedna metoda załatwiająca wszystko 1 linijką kodu. Do tego enum ze stanami (30szt) i enum (lub lista) tranzycji - faktycznie 400 linii.

Nawet jeśli to będzie tylko jeden if, to będzie to 400 zbędnych ifów.

Nie będzie żadnych ifów:

Kopiuj
fun canTransitionBeDone(initState: State, action: Action, listOfTransitions: Transition): Boolean{
  return listOfTransitions.filter(it.initState == initState).filter(it.action == action).map(it.endState) == 1
}

Wystarczy wczytać stan z bazy, i utworzyć obiekt reprezentujący konkretny stan za pomocą metody z danej klasy.

Tak, a co zwróci metoda, która ten obiekt tworzy (czy tam wyciąga z bazy)? Masz fabrykę zwracającą "coś", skąd wiesz co to dokładnie jest? Bo może ci zwrócić obiekt klasy NowyWniosek, który ma metodę zatwierdź(), albo obiekt klasy WniosekAnulowany, który nie ma żadnych tranzycji wychodzących. No chyba, ze wiesz jaki będzie stan tego wniosku wyciąganego z bazy, ale wtedy pozostaje pytanie po co go z niej wyciągać?

Klasa modelu służącego do wyświetlenia danych w liście dla managera, będzie miała wyliczane na podstawie stanu propertisy czy tam metody typu: CanBeCanceled, CanBeApproved, itd.

Czyli kolejne 400 metod? Sprytne, jak płacą ci za linijkę. Jak nie jesteś na wierszówce, to już nie tak sprytne 😉

Po wczytaniu z warstwy danych obiekt ma te metody, które ma, i innych nie można na nim wywołać. Kompilator jest szczęśliwy (i programista też).

Masz metodę findWniosekById(id: Int). Jaki typ wg. ciebie ona deklaruje jako wyjście i co faktycznie zwraca? Bo z tego co rozumiem, twój pomysł opiera się na findWniosekById(id: Int):Wniosek, a w runtime dostajesz jedną z klas dziedziczącą po deklarowanym typie. np. NowyWniosek:Wniosek. Jeżeli tak jest, to skąd wiesz (bez sprawdzania typu rzeczywistego) jaki jest stan obiektu i jakie są na tym typie dozwolone metody?

FA
  • Rejestracja:około 5 lat
  • Ostatnio:około 10 godzin
  • Lokalizacja:warszawa
  • Postów:302
1

Na początek złożoność maszyny stanów. Jeżeli masz 30 możliwych stanów i 400 możliwych przejść pomiędzy tymi stanami, wynika to z wymagań biznesowych, to tak czy inaczej jakoś tę złożoność musisz zakodować. Możesz to zrobić albo za pomocą 30 klas reprezentujących stany, w których znajdzie się łącznie 400 metod, albo pojedynczej klasy reprezentującej stateful object (np. ten wniosek urlopowy), 30 wartości enuma + 400 definicji [initial state, event, result state]. Kodu mniej - więcej tyle samo

Skąd bierze sie takie podjescie, ja nie potrafie sobie kompletnie wyobrazić pomysłu 400 przejsc stanow i 400 metod. To jest ten moment gdy mówi sie ze nie wiemy co poszło nie tak ale jest źle. Dlaczego taki pomysł jest w ogóle brany pod uwage jako rozwiazanie czegokolwiek i to na stacie na czysto?
Jezeli masz 400 czego kolwiek to przeczytanie samych nazw metod/definicji potrwa conajmniej godzine. Co to jest za pomysł, sprzeczy ze zdrowym rozsądkiem.

edytowany 3x, ostatnio: _flamingAccount
Miang
to jest pomysł rzucony po to żeby rzucający wciskał nam ze napisaliśmy bzdurę której nie napisaliśmy ;)
FA
  • Rejestracja:około 5 lat
  • Ostatnio:około 10 godzin
  • Lokalizacja:warszawa
  • Postów:302
0

No prosty use case - manager wyświetla sobie listę wszystkich wniosków podległych mu pracowników. Te wnioski są w różnych stanach. Część jest złożonych, część zatwierdzonych itd. Klika jakiś wniosek do zatwierdzenia. Pobieranie jeszcze raz tego wniosku z bazy, ale tym razem jako klasę specyficzną dla jego stanu jest moim zdaniem mocno bez sensu.
Jeżeli chcesz mapować dane na klasę z metodami odpowiadającymi

Ale dlaczego chcesz pobierać z bazy jeszcze raz cokolwiek? Mieszasz euro z dolarami. Jest stan danych w bazie, wartość i stan maszyny stananów. To są 2 zupełnie rozłaczne kwestie.

Klikasz na wniosek, przekazuje informacje o tym wniosku do klasy bedaca nasza maszyna. Maszyna woła Init() i ustawia sie w odpowiednim stanie, korespodujacym z danymi bazy. Zeby ten init zadziała potrzebna bedzie jakaś fabryka. To jest dodatkowa złoznosc wynikajaca z tego ze implementujesz algorytm który mozna przerwać i kontunuwać w dowolnym momecie, jest to problematyczne z definicji. I wymaga zapisania informacji. Ta informacje mozna nazwać stanem ale to tylko zbieżność nazw. Jak juz przebrniesz przez fabryke, to sytuacja robi sie prosta masz wniosek w stanie A i on moze przejsć do stanu 1, 2... 30 w zaleznosci od akcji, które sie wydarza.

No własnie jakie akcje nas przesowaja do kolejenego stanu.
I to chyba tu sie wszystko pokićkało, bo algorytm który mozna przerwać i kontynuwać i problem kiedy nalezy rozpoczac jego kontynuacje jest problem nie zależnym od problemu maszyny stanów i jak ja reprzentować.

edytowany 2x, ostatnio: _flamingAccount
piotrpo
  • Rejestracja:ponad 7 lat
  • Ostatnio:3 dni
  • Postów:3277
1

Skąd bierze sie takie podjescie, ja nie potrafie sobie kompletnie wyobrazić pomysłu 400 przejsc stanow i 400 metod

Nie wiem, to pytanie raczej do @somekind on postawił taki problem

Jest stan danych w bazie, wartość i stan maszyny stananów. To są 2 zupełnie rozłaczne kwestie.

Wątek zahaczył o to jak przechowywać aktualny stan obiektu (stan w rozumieniu aktualnego położenia w grafie)

Co do kwestii fabryki, prosty przykład modelujemy 2 stabilny wyłącznik. Stany on, off, możliwe akcje turnOn, turnOff

screenshot-20241222165230.png

Podejście, które jak rozumiem proponuje @somekind to taki model:

Kopiuj
public abstract class State{}

public class StateOn extends State{
  public State turnOff(){
    return new StateOff();
  }
}

public class StateOff extends State{
  public State turnOn(){
    return new StateOn();
  }
}

Teraz fabryka - wprowadzam losowość stanu tworzonego obiektu, dla zobrazowania problemu.

Kopiuj

public static State createState(){
  if(System.currentTimeMillis() % 2 ==0){
    return new StateOn();
  } else
  {
    return new StateOff();
  }
}

No i przechodziły do pozyskania obiektu z tej fabryki, które wg. mnie jest upierdliwe:

Kopiuj
public static void changeState(){
  State state == createState();
  if(state instanceof StateOn){
    state(StateOn).turnOff();
  } else if(state == stateOff){
    state(StateOff).turnOff();
  }
}

No i zwyczajnie nie podoba mi się, że muszę sprawdzać typ obiektu zanim będę mógł na nim wywołać metodę.

FA
plus za ładne wytlumaczenie
FA
Dlaczego fabryka jest losowa? Powoduje to ze ignoruje faktyczny stan wylaczników. Wiec albo ja czegos nie rozumiem, albo mamy tu blad logiczny bo stan faktyczny modelu ma znaczenie, bo problem jest zdefiniowany jako "jak przechowywać stan"
piotrpo
Fabryka ma symulować odczyt nieznanego stanu z bazy. Możesz sobie to zamienić na State findState(int id)
FA
(cala odpowiedz mi skasowało) bo wcisnałem skrót klawiszowy z IDE...
FA
  • Rejestracja:około 5 lat
  • Ostatnio:około 10 godzin
  • Lokalizacja:warszawa
  • Postów:302
0

Do zapisania takiego stanu (co było twoim pytaniem) - moim zdaniem wszystko jedno. Można wejść na poziom bazy danych, tego jakie sposoby będą bardziej, a jakie mniej wydajne, ale uważam, że to mało istotne szczegóły.

Po stronie kodu można ten mechanizm implementować rozmaicie. Jednym ze sposobów jest użycie różnych (pod)typów dla rozróżnienia stanu. Jeżeli stan jest przechowywany w programie jako typ obiektu, to żeby go odczytać, trzeba odczytać ten typ.
Tutaj wchodzą moje fobie z Java. Niektóre inne języki mają to rozwiązane lepiej, ale w Java taki kod:

PrivateType object = findObject(.....)
if(object instance of WniosekDoZatwierdzenia){
WniosekDoZatwierdzenia wniosek = (WniosekDoZatwierdzenia) object;
wniosek.zatwierdz();
}
Podpali kompilator w linii z rzutowaniem i zacznie latać uchnecked cast warning. Może nie tragedia, ale nie lubię.

Załóżmy, że chcesz dodać menu kontekstowe z możliwymi akcjami. W jaki sposób to zrobisz? Refleksja i sprawdzanie czy jakaś metoda istnieje? Można, ale sporo energii psu w d....ę.

Próbuje przebic sie przez wątek. patrząc na to chyba tu jest pies pogrzebany. Nie korzysta sie w ten sposób z systemu typów. Mozna ale nie. Klasy reprezentują 2 rodzaje informacji, gołe dane, oraz fukcje do manipulacji nimi. W bazie danych nie zapisuje sie algorytmów, tylko dane. Wiec nie zapisujesz i nie odczytujesz klas bez posrednio. Tylko jakieś krotki/struktury/dto reprezentujace aktualny stan. Klasami reprezentujesz przejscia maszyny stanów, a nie dane. Zwłaszcza gdy chesz algorytm przerywac zapisywać i w znawiac algorytm musi być "czysty" dość w fucyjnym rozumieniu.

Wątek zahaczył o to jak przechowywać aktualny stan obiektu (stan w rozumieniu aktualnego położenia w grafie)

Ja tez by reprezentował klasami, ale w przeciwnieństwie do twojego przypadku miałby jedna metode NextState.

Kopiuj
public abstract class State{
    public State NextState()
}

public class StateOn{
    public State NextState(){
        // zrob cos
        return new StateOff();
    }
}

public class StateOn{
    public State NextState(){
        // zrob cos
        return  new StateOn();
    }
}

Jak widać wszyzstko jest generyczne i nie potrzeba wielu 400 metod, reflekscji kastowania itd.

Do chodzimy do fabryki i tu jest to co poruszalem stan zapisany a stan reprezentowany przez klasy to kwestie rozłaczne. W przypadku nie których algorytmow konieczne bedzie zapisanie nazwy stanu bezposrednio, w przypatku naszego przelacznika jest prosto.

Kopiuj
    public static State Init(){
       var modelState = GetModelState();
       if(modelState.isOn()){
         return new StateOn(); 
       }
       else{
           return new StateOff();
       }
    }

Znowu nie ma kastowania, 400 metod, 400 definicji przejśc tylko smutna konieczność zwiazanaz z odczytaniem danych. Chce sie odciać od dyskusji, czy moze zrobic to framework za nas, czy mozna napisac własny, nakład pracy to 3 linie na każdy nowy stan, wiec jest prawie zaden.

Finalnie otrzymasz cos takiego:

Kopiuj
class StateMachine
{
    public State currentState;

    public  StateMachine{
           currentState = Init();
    }

    public void Start(){
        while(currentState != null){
            currentState = currentState.nextState();
        }
    }

Nie masz 400 metod, czy definicji, nie urzywasz refleksji, castowanie, Klasy reprezentuja przejscia miedzy stanami i głupie przejścia są nie mozliwe bo zwyczajnie nie są zaimplementowane, wiec nie trzba ich wspierać. Podejscie skaluje sie dowolnie, liniowo w raz z rozmiarami grafu który reprezentujesz. Dodatkowo wstanie zapisujesz faktyczny stan a nie mieszasz go z logika. Jeze pojawi sie 3 stan do którego przechodzi sie po szybkim nacisnieciu guzika 2 razy, to mozna w nim zapisac co trzeba bez mieszania w logike.

edytowany 2x, ostatnio: _flamingAccount
piotrpo
  • Rejestracja:ponad 7 lat
  • Ostatnio:3 dni
  • Postów:3277
0

Ja tez by reprezentował klasami, ale w przeciwnieństwie do twojego przypadku miałby jedna metode NextState.

W przypadku włącznika to zadziała, bo tu mamy bardzo prosty graf. Jednak FSM pozwala na więcej niż jedną tranzycję wyjściową ze stanu.
W dodatku to nie jest mój przypadek, tylko to to jak ja rozumiem propozycję @somekind , którą uwazam za niezbyt szczęśliwą.

To jak ja bym to zrobił, a właściwie zrobiłem można sobie obejrzeć tutaj: https://github.com/piotrpo/SimpleFsm
Nie, żebym się upierał, że to w każdym przypadku super implementacja, ale dla przypadku w którym było to faktycznie użyte było całkiem spoko..

FA
  • Rejestracja:około 5 lat
  • Ostatnio:około 10 godzin
  • Lokalizacja:warszawa
  • Postów:302
0

Przy większej ilości stanów jest analogicznie.
Piszesz klasy w ilości równej ilości węzłów które graf reprezentuje.
A w metodzie next state piszesz ilość if drabinek w liczbie krawedzi jaka wychodzi z węzła.

Skaluje się 1:1 w skali rozmiarem grafu.

Twoja implementacja jest niezbyt fortunna, ale niech będzie że to pomysł kogoś innego.
Tak i tak, da rade zapisać to na klasach żeby się skalowalo

edytowany 2x, ostatnio: _flamingAccount
somekind
Moderator
  • Rejestracja:około 17 lat
  • Ostatnio:około 6 godzin
  • Lokalizacja:Wrocław
0
piotrpo napisał(a):

Jakie 400 metod w jednej klasie? Jedna metoda załatwiająca wszystko 1 linijką kodu. Do tego enum ze stanami (30szt) i enum (lub lista) tranzycji - faktycznie 400 linii.

Jedna jednolinjkowa metoda realizująca całą logikę biznesową systemu? Np. inicjująca zamówienie, dodająca do niego pozycje, generująca faktury, wysyłająca towar?
Wow, to już wiem, po co teraz takie szerokie monitory sprzedają. ;)
No chyba, że ta metoda, to po prostu eval. :P

Maszyna stanów nie istnieje sama dla siebie, mamy wieloetapowy proces biznesowy do zamodelowania. I chodzi o to, żeby kod zarządzania legalnymi ruchami po grafie nie zaciemniał nam realnego kodu robiącego coś przydatnego. Jak mamy skomplikowany proces i wszystko w jednej klasie, to to wszystko strasznie puchnie.

Kopiuj
fun canTransitionBeDone(initState: State, action: Action, listOfTransitions: Transition): Boolean{
  return listOfTransitions.filter(it.initState == initState).filter(it.action == action).map(it.endState) == 1
}

No dobra, ale to tylko sprawdzenie czy można coś zrobić. Trzeba to wywołać w każdej metodzie biznesowej - czyli dla 400 metod będzie 400 zbędnych wywołań canTransitonBeDone. I do każdego z nich trzeba napisać testy.

No chyba, ze wiesz jaki będzie stan tego wniosku wyciąganego z bazy, ale wtedy pozostaje pytanie po co go z niej wyciągać?

Chyba po to, żeby kontynuować proces biznesowy.

Czyli kolejne 400 metod? Sprytne, jak płacą ci za linijkę. Jak nie jesteś na wierszówce, to już nie tak sprytne 😉

Robisz GUI na 400 przycisków do jednego wiersza? Użytkownicy jeszcze nie zawieźli Cię do lasu i nie kazali wykopać sobie grobu?

To jest ogólnie problem całkowicie ortogonalny do sposobu implementacji maszyny stanów, bo używanie jednej klasy do modelowania przepływu biznesowego oraz wyświetlania GUI, mocno śmierdzi łamaniem podstaw BHP.

Masz metodę findWniosekById(id: Int). Jaki typ wg. ciebie ona deklaruje jako wyjście i co faktycznie zwraca? Bo z tego co rozumiem, twój pomysł opiera się na findWniosekById(id: Int):Wniosek, a w runtime dostajesz jedną z klas dziedziczącą po deklarowanym typie. np. NowyWniosek:Wniosek. Jeżeli tak jest, to skąd wiesz (bez sprawdzania typu rzeczywistego) jaki jest stan obiektu i jakie są na tym typie dozwolone metody?

Nie no, pisałem do @Klaun wcześniej - wczytujesz obiekt typu, którego potrzebujesz. Warstwa danych ma oddzielne metody, które zwracają obiekty dla poszczególnych przypadków użycia. Jeśli rekord pod danym id nie ma oczekiwanego stanu, to zwracamy error i tyle.

_flamingAccount napisał(a):

Skąd bierze sie takie podjescie, ja nie potrafie sobie kompletnie wyobrazić pomysłu 400 przejsc stanow i 400 metod. To jest ten moment gdy mówi sie ze nie wiemy co poszło nie tak ale jest źle. Dlaczego taki pomysł jest w ogóle brany pod uwage jako rozwiazanie czegokolwiek i to na stacie na czysto?
Jezeli masz 400 czego kolwiek to przeczytanie samych nazw metod/definicji potrwa conajmniej godzine. Co to jest za pomysł, sprzeczy ze zdrowym rozsądkiem.

Przy 30 stanach możliwych jest 435 przejść. O ile dobrze pamiętam, to się kombinacja bez powtórzeń nazywa. Oczywiście mogę się mylić, ktoś może poprawić obliczenia - zdawałem maturę ponad 20 lat temu.

Użyłem tej liczby jako pesymistycznego przykładu na ilość kodu, który powstanie dla złożonego procesu przy podejściu jedna klasa + status w enumie. Oczywiście w praktyce będzie tego mniej, strzelam, że w praktyce masz możliwe 2-4 ścieżki z każdego stanu, więc tych metod będzie mniej niż 100.

edytowany 3x, ostatnio: somekind
piotrpo
  • Rejestracja:ponad 7 lat
  • Ostatnio:3 dni
  • Postów:3277
0

No dobra, ale to tylko sprawdzenie czy można coś zrobić. Trzeba to wywołać w każdej metodzie biznesowej - czyli dla 400 metod będzie 400 zbędnych wywołań canTransitonBeDone. I do każdego z nich trzeba napisać testy.

Kopiuj
fun targetState(initState: State, action: Action, listOfTransitions: Transition[]): State{
  return listOfTransitions.filter(it.initState == initState).filter(it.action == action).map(it.endState)?:initState
}

I masz funkcję zwracającą stan docelowy. Do jej przetestowania potrzebujesz na oko 2 testów.

Oczywiście to musi być opakowane w jakąś logikę biznesową:

Kopiuj
fun onCancelClicked(){
  wniosek.state = targetState(wniosek.state, ACTION.CANCEL, listOfTransitions)
  repository.save(wniosek)
}

Jeżeli takich akcji jest 400, to będziesz potrzebował 400 takich metod. Ale realnie, to nawet jeżeli mamy te 30 możliwych stanów, w których wniosek może zostać anulowany, to akcja cancel będzie jedna. Czyli wystarczy jeden test sprawdzający dla każdego ze stanów, czy po wywołaniu tej akcji stanem wynikowym jest STATE.CANCELED.

FSM to nic więcej niż graf skierowany, w którym wierzchołkami są stany, krawędzie nazwane, a cała funkcjonalność zamyka się w prostej funkcji f(wierzchołek, krawędź) -> wierzchołek.
Jeżeli jest to graf, to nie widzę powodu, żeby to traktować inaczej niż graf. Można, ale po co? Pozostanie przy grafie pozwala na korzystanie z całego dorobku związanego z grafami. Szukanie najkrótszej drogi, sprawdzanie poprawności definicji. Prawdopodobnie nie będzie takiej potrzeby, ale trzymanie się znanych ścieżek jest bezpieczniejsze.

Chyba po to, żeby kontynuować proces biznesowy.

Pobierasz listę tych wniosków i wyświetlasz ją jak listę. Mają różne stany reprezentowane wg. ciebie przez typ obiektu. Skąd wiesz przed ich pobraniem której z 30 metod dostępu do danych użyć? Ile będzie trwało zaimplementowanie prostego wymagania biznesowego "wyślij email do autora wniosku przy wywołaniu cancel na dowolnym ze stanów"?

FA
  • Rejestracja:około 5 lat
  • Ostatnio:około 10 godzin
  • Lokalizacja:warszawa
  • Postów:302
0

Jeżeli jest to graf, to nie widzę powodu, żeby to traktować inaczej niż graf. Można, ale po co? Pozostanie przy grafie pozwala na korzystanie z całego dorobku związanego z grafami. Szukanie najkrótszej drogi, sprawdzanie poprawności definicji. Prawdopodobnie nie będzie takiej potrzeby, ale trzymanie się znanych ścieżek jest bezpieczniejsze.

Graf to dobry punkt wyjścia ale to nie pełny model. Graf w nie reprezentuje ani stanu ani algorytmu, a maszna stanów ma oba. Korzystajac z przykładu grafów. Implementujesz gre, mape mozna zareprezentowac grafem, zachowanie "NPC" które patoroluje mape maszyna stanow. Npc innaczej sie zachowa gdy jest blisko ciebie innaczej gdy daleko innaczej gdy wlaczy sie alarm lub inny specjalny state. Ten sam NPC bedzie zachowywał sie innaczej na tej samej pozycji na mapie w zaleznosci od stanu gry. Argumentacja ze stan tez jest grafem mnie nie przekona, bo nie zaimplemetujesz 4 miliardow mozliwych roznic w intach jako graf.

WeiXiao
  • Rejestracja:około 9 lat
  • Ostatnio:około 9 godzin
  • Postów:5108
1

switch statement

FA
  • Rejestracja:około 5 lat
  • Ostatnio:około 10 godzin
  • Lokalizacja:warszawa
  • Postów:302
0

@piotrpo

Pobierasz listę tych wniosków i wyświetlasz ją jak listę. Mają różne stany reprezentowane wg. ciebie przez typ obiektu. Skąd wiesz przed ich pobraniem której z 30 metod dostępu do danych użyć? Ile będzie trwało zaimplementowanie prostego wymagania biznesowego "wyślij email do autora wniosku przy wywołaniu cancel na dowolnym ze stanów"?

Tak jak @somekind pisał nie implemenujesz 30 metod. Ich nie ma. I nie ma tego problemu.
Jeżeli zamiast metod miało byc którego stanu uzyc, przed pobraniem danych. to to jest głupie. Nie da sie, najpiew pobierasz potrzebna informacje potem decydujes z definicji.

Ile będzie trwało zaimplementowanie prostego wymagania biznesowego "wyślij email do autora wniosku przy wywołaniu cancel na dowolnym ze stanów"?

Kolejne mało metytoryczne pytanie.
Minute, 5min? znajdujesz klase reprezentujaca anulowanie, do pisujesz wysłanie maila. Czas na kawe.
[edit]
A jak masz bałagan w kodzie, robisz rename statu Cancled and CancelInProgres. IDE zmienia wszystkie referecje za ciebie. A nowy stan CancelInProgres wskazuje na stary stanCancelled, lub Error. I idzie na kawe.

edytowany 1x, ostatnio: _flamingAccount
Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)