Jak sensownie zaimplementować maszynę stanów?

Jak sensownie zaimplementować maszynę stanów?
LukeJL
  • Rejestracja:około 11 lat
  • Ostatnio:około 7 godzin
  • Postów:8406
1

Wg mnie wewnętrznie obiekty mogą sobie być w różnych stanach, jednak to, w jaki sposób dane obiekty współpracują ze sobą, to trochę co innego. Możliwe, że nie ma sensu tworzyć globalnie stanów, w których jest cała aplikacja (stany mogą być poukrywane w poszczególnych obiektach).

Jeśli obiekt A jest zależny od obiektu B, to niekoniecznie musi znać jego przestrzeń stanów. B może reprezentować pewną akcję (np. przygotowanie dokumentu) i może obiekt A jest zainteresowany tylko tym, czy się powiodła akcja albo jaką wartość finalnie zwróciła (albo jaki błąd).

Plus wydaje mi się, że dyskusja dotyczy częściowo maszyn stanów, a częściowo architektury opartej o eventy. Może to właśnie o eventach powinniśmy więcej pisać.

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

No to łapiemy globalnie ten event cancel i go obsługujemy.


edytowany 4x, ostatnio: LukeJL
FA
Dokładnie, pomieszane jest kilka tematów na raz. Eventy, w znawainie egzekucji, maszyny stanów, zapisywanie stanow, jak zrobic combox, jak cos wczytac bez wczytywania.
piotrpo
  • Rejestracja:ponad 7 lat
  • Ostatnio:3 dni
  • Postów:3277
0

@_flamingAccount

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

Do reprezentacji aktualnego stanu (wierzchołka w grafie) potrzebujesz 1 pola. Algorytm poruszania się po grafie, to 1 linijka mieszcząca się na standardowym ekranie.

Minute, 5min? znajdujesz klase reprezentujaca anulowanie, do pisujesz wysłanie maila.

NIe bardzo, bo przy 30 stanach masz 30 Klas, z których każda ma własną implementację. Akcja X może powodować zmian A -> B, C->D.

@WeiXiao

switch statement

Tak, można mieć switch po typach sealed. O punkt lepiej.

@_flamingAccount

Kolejne mało metytoryczne pytanie.

Moim zdaniem bardzo merytoryczne - podatność na zmiany, to jedno z podstawowych kryteriów jakości kodu.

Argumentacja ze stan tez jest grafem mnie nie przekona, bo nie zaimplemetujesz 4 miliardow mozliwych roznic w intach jako graf.

Co to wnosi do dyskusji? Wiadomo, że kalkulator działający na int, czy float dałoby się teoretycznie zapisać jako graf stanów. Wiadomo też, że to bez sensu. Hindusi aż tak tani nie są.

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.

No to jest głupie, dlatego to krytykuję. Pobieram jakieś dane, odczytuję stan, pakuję to w odpowiadający mu wrapper z metodami możliwymi do wywołania i go zwracam. No i w jaki sposób metoda która tego obiektu potrzebuje, ma wiedzieć co to za typ specyficzny?

@LukeJL

Wg mnie wewnętrznie obiekty mogą sobie być w różnych stanach, jednak to, w jaki sposób dane obiekty współpracują ze sobą, to trochę co innego. Możliwe, że nie ma sensu tworzyć globalnie stanów, w których jest cała aplikacja (stany mogą być poukrywane w poszczególnych obiektach).

No możliwe. Można przecież taki wniosek urlopowy przypisywać do stanu na podstawie daych w nim zawartych, według jakiejś reguły, np. "jeżelii data zatwierdzenia przez kierownika not null, to zatwierdzony". Zwykle prowadzi to do wypasionej drabinki if'ów, ale są też przypadki, w których taka prosta walidacja wystarczy.

No to łapiemy globalnie ten event cancel i go obsługujemy.

Owszem, jeżeli ten event istnieje i jest jakieś miejsce w którym da się go przechwycić globalnie. W moim przekonaniu, mając 30 stanów, 30 klas je reprezentujących jest też 30 miejsc w których należałoby dopisać wyślijMaila()

No i właściwie jaki macie problem z implementacją, którą proponuję? jedna funkcja eval(stan, akcja) -> stan? Jakie niedostatki tutaj widzicie i dlaczego zrobienie obiektowej struktury klas niby jest lepsze?

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

NIe bardzo, bo przy 30 stanach masz 30 Klas, z których każda ma własną implementację. Akcja X może powodować zmian A -> B, C->D.

Nie rozumiem Cie...
albo: zafixowałeś sie na tym grafie tak bardzo że nie do puszczasz do siebie informacji. Dostałeś nawet insturkcje jak dodac kolejny stan jezli masz zwalony model i to ignorujesz.
albo: mieszasz tak wiele różnych tematów naraz ze nie ma sznas w formie pisanej sie z tobą dogadać.
albo: bronisz chochoła, wymyśliłes sobie głupi przykład i ududowaniasz ze jest głupi, super fajnie przestań tak robić,
albo: po prostu nie potrafisz programować obiektowo? Twoje przykłady by na to wskazywały
albo: nowinka z przed 5 lat z Net, nie dostarła jeszcze do JVM, z przerzucanie pracy z progamisty na kompilator to nie znany koncept i nie wiesz ze sie da i to działa.
albo: po trochu wszystko naraz.

Co to wnosi do dyskusji? Wiadomo, że kalkulator działający na int, czy float dałoby się teoretycznie zapisać jako graf stanów. Wiadomo też, że to bez sensu. Hindusi aż tak tani nie są.

Miało uniknać idiotycznych kometarzy, ale musialeś. Jak sam argumentujesz, stan twojej aplikacji i stan maszyny stanów to kwestie rozłaczne.

Do reprezentacji aktualnego stanu (wierzchołka w grafie) potrzebujesz 1 pola. Algorytm poruszania się po grafie, to 1 linijka mieszcząca się na standardowym ekranie.

Dyskuja z Toba nie ma sensu bo ignorujesz dostarczane do Ciebie informacje, 3? raz ignorujesz informacje ze stan appliakcji i maszy stanów to rozłaczne kwestie oraz nie wiem ile razy juz z ignorwałes wzmanki o warunkach przejscia/algorytmnie. Twoje rozumowanie ma olbrzymie luki, ale nie trzeba sie nimi przejmowac gdy w kólko powtarza sie to samo ignorujac rozmówców...

Moim zdaniem bardzo merytoryczne - podatność na zmiany, to jedno z podstawowych kryteriów jakości kodu.

Mam w pracy kolege co uzywa podbnych argumentow i tez nikt go nie rozumie i dogadać sie z nikim nie moze. To co robisz nie jest merytoryczne.

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

Ech...

edytowany 1x, ostatnio: piotrpo
AN
  • Rejestracja:prawie 11 lat
  • Ostatnio:30 minut
  • Postów:973
2

@somekind wrzuciłbyś jakiś prosty przykład? Zainteresowało mnie Twoje podejście ale nie rozumiem jakby to miało działać np. dostajemy request, że chcemy anulować wniosek o ID: 123. Skąd kompilator ma wiedzieć czy ten wniosek można anulować skoro na etapie kompilowania nie ma takiej informacji?


Zdalna praca dla Senior Python Developerów --> PW
AN
  • Rejestracja:prawie 11 lat
  • Ostatnio:30 minut
  • Postów:973
0

Tutaj znalazłem nową libkę, którą ktoś wrzucił na reddita RUST
https://github.com/eboody/statum

Posiada validacje na etapie kompliacji czyli się da. Ale nadal bez ifów się nie obędzie w przypadku czytania z DB w przykładzie w README jest np:

    if self.state != "review" {
        return Err(Error::InvalidState);
    }

Zdalna praca dla Senior Python Developerów --> PW
KL
  • Rejestracja:12 miesięcy
  • Ostatnio:minuta
  • Postów:379
0

Zupełnie przypadkiem trafiłem na występ Sławka Sobótki, który dotyczył modelowania procesu obiegu dokumentu, czyli w sumie to o czym tutaj była mowa od początku.

Tam w pewnym momencie pojawiło się wymaganie dotyczące konfigurowalności takiego procesu, więc naturalnie skończyło się na wersji podobnej do tej sugerowanej tutaj przez @piotrpo, gdzie klasa reprezentująca stan przechowywała możliwe przejścia wraz z predykatami i akcjami wykonywanymi po udanej zmianie stanu (znacznik czasowy - 44:50). Całość konfigurowalna z kodu.

Pokazane zostało również podejście:

  • z jedną klasą mającą metody na wszystkie możliwe akcje do wykoanania, a w nich ifowanie (znacznik czasowy - 11:00)
  • klasa per stan dokumentu (znacznik czasowy - 41:55), ale trochę inne niż to proponowane tutaj przez @somekind, bo tam klasa dokumentu ma w sobie obiekt reprezentujący stan na którym wykonywane są akcje

Ja osobiście szczerze mówiąc gdybym implementował coś podobnego to pewnie zaczął bym od:

  • jednej klasy z metodami określającymi akcje biznesowe i ifami
  • dopiero gdyby proces obiegu się komplikował, albo miał być konfigurowalny to bym refaktorował w kierunku innych podejść 😀
edytowany 2x, ostatnio: Klaun
somekind
Moderator
  • Rejestracja:około 17 lat
  • Ostatnio:około 6 godzin
  • Lokalizacja:Wrocław
0
anonimowy napisał(a):

@somekind wrzuciłbyś jakiś prosty przykład? Zainteresowało mnie Twoje podejście ale nie rozumiem jakby to miało działać np. dostajemy request, że chcemy anulować wniosek o ID: 123. Skąd kompilator ma wiedzieć czy ten wniosek można anulować skoro na etapie kompilowania nie ma takiej informacji?

Kompilator nie będzie wiedział, czy wniosek 123 można anulować na poziomie otrzymania requestu. Po prostu nasz kod odpowiadający za obsługę żądań anulowania wczyta z warstwy danych wniosek nadający się do anulowania, a potem wywoła odpowiednią metodę.
Mam na myśli coś takiego:
A. Domena:

Kopiuj
public abstract class HolidayConclusionBase
{
    public Guid Id { get; protected set; }
}

public interface IApprovableHolidays
{
    ApprovedHolidayConclusion Approve(string approvedBy, DateTimeOffset approvedDate);
};

public interface ICancellableHolidays
{
    CancelledHolidayConclusion Cancel(string cancelledBy, DateTimeOffset cancelledDate);
};

public class NewHolidayConclusion : HolidayConclusionBase, IApprovableHolidays, ICancellableHolidays
{
    public string EmployeeName { get; init; }
    public DateTimeOffset StartDate { get; init; }
    public DateTimeOffset EndDate { get; init; }

    public NewHolidayConclusion(Guid id, string employeeName, DateTimeOffset startData, DateTimeOffset endData)
    {
        Id = id;
        EmployeeName = employeeName;
        StartDate = startData;
        EndDate = endData;
    }

    public RejectedHolidayConclusion Reject(string rejectedBy, DateTimeOffset rejectedDate, string rejectedReason) =>
        new(Id, rejectedBy, rejectedDate, rejectedReason);

    public ApprovedHolidayConclusion Approve(string approvedBy, DateTimeOffset approvedDate) => new(Id, approvedBy, approvedDate);

    public CancelledHolidayConclusion Cancel(string cancelledBy, DateTimeOffset cancelledDate) => new(Id, cancelledBy, cancelledDate);
}

public class ApprovedHolidayConclusion : HolidayConclusionBase, ICancellableHolidays
{
    public string ApprovedBy { get; init; }
    public DateTimeOffset ApprovedDate { get; init; }

    public ApprovedHolidayConclusion(Guid id, string approvedBy, DateTimeOffset approvedDate)
    {
        Id = id;
        ApprovedBy = approvedBy;
        ApprovedDate = approvedDate;
    }

    public CancelledHolidayConclusion Cancel(string cancelledBy, DateTimeOffset cancelledDate) => new(Id, cancelledBy, cancelledDate);
}

public class RejectedHolidayConclusion : HolidayConclusionBase, IApprovableHolidays
{
    public string RejectedBy { get; init; }
    public DateTimeOffset RejectedDate { get; init; }
    public string RejectionReason { get; }

    public RejectedHolidayConclusion(Guid id, string rejectedBy, DateTimeOffset rejectedDate, string rejectionReason)
    {
        Id = id;
        RejectedBy = rejectedBy;
        RejectedDate = rejectedDate;
        RejectionReason = rejectionReason;
    }

    public ApprovedHolidayConclusion Approve(string approvedBy, DateTimeOffset approvedDate) => new(Id, approvedBy, approvedDate);
}

public class CancelledHolidayConclusion : HolidayConclusionBase
{
    public CancelledHolidayConclusion(Guid id, string cancelledBy, DateTimeOffset cancelledDate)
    {
        Id = id;
        CancelledBy = cancelledBy;
        CancelledDate = cancelledDate;
    }

    public string CancelledBy { get; }
    public DateTimeOffset CancelledDate { get; }
}

B. Logika aplikacji:

Kopiuj
public record CreateHolidaysRequest(string EmployeeName, DateTimeOffset StartDate, DateTimeOffset EndDate)
    : IRequest<Guid>;

public class CreateHolidaysUseCase(IConclusionsRepository conclusionsRepository)
    : IRequestHandler<CreateHolidaysRequest, Guid>
{
    public async Task<Guid> Handle(CreateHolidaysRequest request, CancellationToken cancellationToken)
    {
        var holidayConclusion = new NewHolidayConclusion(Guid.NewGuid(), request.EmployeeName, request.StartDate, request.EndDate);
        var id = await conclusionsRepository.Upsert(holidayConclusion);
        return id;
    }
}

public record ApproveHolidaysRequest(Guid Id) : IRequest<ApproveHolidaysRequest>;

public class ApproveHolidayUseCase(IConclusionsRepository conclusionsRepository)
    : IRequestHandler<ApproveHolidaysRequest, ApproveHolidaysRequest>
{
    public async Task<ApproveHolidaysRequest> Handle(ApproveHolidaysRequest request, CancellationToken cancellationToken)
    {
        var existingConclusion = await conclusionsRepository.GetForApproval(request.Id);
        var approvedConclusion = existingConclusion.Approve("Dyrektorek", DateTimeOffset.UtcNow);
        await conclusionsRepository.Upsert(approvedConclusion);
        return request;
    }
}

public record RejectHolidaysRequest(Guid Id, string Reason) : IRequest<RejectHolidaysRequest>;

public class RejectHolidayUseCase(IConclusionsRepository conclusionsRepository)
    : IRequestHandler<RejectHolidaysRequest, RejectHolidaysRequest>
{
    public async Task<RejectHolidaysRequest> Handle(RejectHolidaysRequest request, CancellationToken cancellationToken)
    {
        var existingConclusion = await conclusionsRepository.GetForRejection(request.Id);
        var approvedConclusion = existingConclusion.Reject("Dyrektorek", DateTimeOffset.UtcNow, request.Reason);
        await conclusionsRepository.Upsert(approvedConclusion);
        return request;
    }
}

public record CancelHolidaysRequest(Guid Id) : IRequest<CancelHolidaysRequest>;

public class CancelHolidayUseCase(IConclusionsRepository conclusionsRepository)
    : IRequestHandler<CancelHolidaysRequest, CancelHolidaysRequest>
{
    public async Task<CancelHolidaysRequest> Handle(CancelHolidaysRequest request, CancellationToken cancellationToken)
    {
        var existingConclusion = await conclusionsRepository.GetForCancellation(request.Id);
        var approvedConclusion = existingConclusion.Cancel("Pokorny pracownik", DateTimeOffset.UtcNow);
        await conclusionsRepository.Upsert(approvedConclusion);
        return request;
    }
}

public record GetAllHolidaysRequest : IRequest<IEnumerable<HolidaysViewModel>>;

public class GetAllHolidaysUseCase(IHolidaysViewModelReader vmReader)
    : IRequestHandler<GetAllHolidaysRequest, IEnumerable<HolidaysViewModel>>
{
    public async Task<IEnumerable<HolidaysViewModel>> Handle(GetAllHolidaysRequest request, CancellationToken cancellationToken)
    {
        return await vmReader.GetAll();
    }
}

Jak widać, w przypadkach użycia nie musimy sprawdzać żadnych statusów wniosku. Skoro wczytaliśmy z bazy, to możemy na nim wywołać metodę domeny i wszystko. Nie trzeba do tego testów jednostkowych, skompilowało się, więc działa.

PS, jeśli komuś wydaje się, że wniosek urlopowy to nie jest holiday conclusion, to niech się lepiej dwa razy zastanowi. Nie takie rzeczy widziałem w kodzie pisanym przez polskich señorów z angielskim C3. ;)

edytowany 1x, ostatnio: somekind
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)