1
\$\begingroup\$

The goal is to have well defined state transitions, and the ability to provide the next event to execute.

I'd like to know if this is a proper implementation of State Machine, considering how states and transitionTable are defined, and how I handle event as input and output via nextEvent.

In many examples I cannot clearly define verbiage for state, so I defined them as verbs (notice ing suffix), as if it represents the ongoing progress of workflow. This may be wrong..

I also defined multiple events that could occur for a single state. For example, GetDeviceStatus and GetIntegrityStatus events both occur in RetrievingStatus state. This is also the case for Downloading state. You can see in the cases, when determining the next event, I need to check what the previous state was first.

If flawed, what are the pitfalls to my design, and how could it be improved? thanks.

enum ExampleState {
    case Initiate
    case Authorizing 
    case RetrievingStatus 
    case Downloading 
    case Confirming 
    case End
}

enum ExampleEvent {
    case InitiateSequence
    case GetAuthToken
    case GetDeviceStatus
    case GetIntegrityStatus
    case DownloadFromServer
    case DownloadToDevice
    case Confirm
}


class StateMachine {

var oldState: ExampleState!
var currentState: ExampleState!
var currentEvent: ExampleEvent!

var table: [ExampleState: [ExampleEvent: ExampleState]] = [.Initiate:         [.InitiateSequence:   .Authorizing],
                                                           .Authorizing:      [.GetAuthToken:       .RetrievingStatus],
                                                           .RetrievingStatus: [.GetDeviceStatus:    .Downloading, .GetIntegrityStatus: .Confirming],
                                                           .Downloading:      [.DownloadFromServer: .Downloading, .DownloadToDevice: .RetrievingStatus],
                                                           .Confirming:       [.Confirm:            .End]]

func nextEvent(event: ExampleEvent) -> EventExecutor? {

    let transitionState = table[currentState]![event]!
    let oldState = currentState

    switch (transitionState) {

        case .Initiate:
            currentState = .Initiate
            return InitiateSequence()

        case .Authorizing:
            currentState = .Authorizing
            return GetAuthToken()

        case .RetrievingStatus:
            currentState = .RetrievingStatus
            switch (oldState) {
            case .Authorizing: return GetDeviceStatus()
            case .Downloading: return GetIntegrityStatus()
            default: return nil
            }

        case .Downloading:
            currentState = .Downloading
            switch (oldState) {
            case .RetrievingStatus: return DownloadFromServer()
            case .Downloading: return DownloadToDevice()
            default: return nil
            }

        case .Confirming:
            currentState = .Confirming
            return Confirm()

        case .End:
            currentState = .End
            return nil
    }

}
}

Use case

@objc func rxExecute() {
    publisher
        .map { self.stateMachine.nextEvent(event: $0) } // $0 == event that just completed
        .flatMap { $0!.rxExecute() } // execute next event from state machine output
        .subscribe(subscriber) // handles errors/success from completed event
}
\$\endgroup\$
1

1 Answer 1

1
\$\begingroup\$

First there are a few things about design

Does you machine has something like 'error' that is not handled (e.g. by throwing an exception) or you have only 'outcomes' that handled in the same matter.

Do you expect branches in the FSM? Is it possible for an fsm method to fail? If possible how it is handled? E.g. ignored (say we rely on caller to retry on timer), raise failure event and queue it for execution, use return value as a guard to branch in state machine method.

Do you expect your code to maintained over 20+ years period?

After many attempts to make better existing FSMs, making my own, trying different frameworks and making my own frameworks too, I came to a trivially simple conclusion (aka 'dumb' FSM pattern): each event should be represented by a function and each function should contain a switch by state; or every state should be represented by a function and every function should contain a switch by event.

In the long run this is the cheapest solution. E.g. if you do not have branches/failures one can write a simple table driven fsm, however, once we will get the first branch/failure to handle we will need to either make it way more complicated or throw it away and rewrite using the 'dumb' approach

\$\endgroup\$
2
  • \$\begingroup\$ Ok thanks. I think I understand, but it would help if you could share an examples of this. In my case, the state machine wont be responsible for handling any errors except invalid state transitions. The ResponseSubscriber actually handles the errors from the actual event task, and if there are no errors I save and emit completed event to publisher. I'll add code, and would like your opinion of this approach. \$\endgroup\$
    – J. Doe
    May 19, 2020 at 19:28
  • 1
    \$\begingroup\$ Your simple fsm, your code will work as is and it is very close to the 'dumb' one I described. The only difference is that you have one switch statement to handle all combinations of state/and events and a table to figure out next state. And the real 'dumb' one will have one switch per state that will perform actions and figure out next state. \$\endgroup\$
    – zzz777
    May 19, 2020 at 20:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.