Skip to content

Instantly share code, notes, and snippets.

@jemmons
Created February 2, 2015 15:05
Show Gist options
  • Save jemmons/c9434cc09831a276003e to your computer and use it in GitHub Desktop.
Save jemmons/c9434cc09831a276003e to your computer and use it in GitHub Desktop.
import Foundation
class StateMachine<P:StateMachineDelegateProtocol>{
private unowned let delegate:P
private var _state:P.StateType{
didSet{
delegate.didTransitionFrom(oldValue, to:_state)
}
}
var state:P.StateType{
get{ return _state }
set{
if delegate.shouldTransitionFrom(_state, to:newValue){
_state = newValue
}
}
}
init(initialState:P.StateType, delegate:P){
_state = initialState
self.delegate = delegate
}
}
protocol StateMachineDelegateProtocol: class{
typealias StateType
func shouldTransitionFrom(from:StateType, to:StateType)->Bool
func didTransitionFrom(from:StateType, to:StateType)
}
class MyClass{
private var machine:StateMachine<MyClass>!
enum AsyncNetworkState{
case Ready, Fetching, Saving
case Success(NSDictionary)
case Failure(NSError)
}
init(){
machine = StateMachine(initialState: .Ready, delegate: self)
}
}
extension MyClass:StateMachineDelegateProtocol{
typealias StateType = AsyncNetworkState
func shouldTransitionFrom(from:StateType, to:StateType)->Bool{
switch (from, to){
case (.Ready, .Fetching), (.Ready, .Saving),
(.Fetching, .Success), (.Fetching, .Failure),
(.Saving, .Success), (.Saving, .Failure):
return true
case (_, .Ready):
return true
default:
return false
}
}
func didTransitionFrom(from:StateType, to:StateType){
switch (from, to){
case (.Ready, .Fetching):
MyAPI.fetchRequestWithCompletion(handleRequest)
case (.Ready, .Saving):
MyAPI.fetchRequestWithCompletion(handleRequest)
case (_, .Failure(let error)):
displayGeneralError(error)
machine.state = .Ready
case (.Fetching, .Success(let json)):
parseFetchSpecificJSON(json)
machine.state = .Ready
case (.Saving, .Success(let json)):
parseSaveSpecificJSON(json)
machine.state = .Ready
case (_, .Ready):
updateInterface()
default:
break
}
}
}
private extension MyClass{
func handleRequest(json:NSDictionary, error:NSError?){
if let someError = error{
machine.state = .Failure(someError)
} else{
machine.state = .Success(json)
}
}
}
@algal
Copy link

algal commented Feb 3, 2015

Hi, this is very cool and an interesting approach! I had a question.

Right now, a client needs to defines four things:

  1. the StateType enum defining the possible states,
  2. the shouldChange function that defines allowed transitions,
  3. the didChange function that defines effects of transitions, and
  4. the delegate implementation that binds these together, and does a bit more

I'm wondering if there's an alternative design which does not require the fourth. Did you consider an approach where the transition and effect functions are simply required as methods on the enum type? This might be simpler to use, because then the client only needs to provide a single conforming type, the enum.

One difficulty is that the delegate object is not just binding together the enum and those functions. It's also specifying the machine's initial state via its constructor, and it's got a property definition and initializer. But these two bits feel so boilerplate, I wonder if they can be handled generically by the framework rather than the client.

@jemmons
Copy link
Author

jemmons commented Feb 3, 2015

@algal A fascinating idea! And one that casts doubt on my assertion that "a delegate with value semantics doesn't really make any sense." After all, if we have to implement the enum anyway, why not include the implementation of our "delegate" methods there as well.

I'll have to try this out. I admit the biggest influence on the current design is how I'm used to using delegates in ObjC. Your suggestion may be a more Swifty idiom.

@algal
Copy link

algal commented Feb 4, 2015

Yes, another thing I notice here is that with this design it feels like your "delegate" actually is the state machine. For instance, if someone handed you the delegate object at runtime, and didn't tell you any type names except for its method name and the enum member states, it would look like a freestanding state machine from its interface, rather than the delegate of some other kind of thing.

I think this suggests that the generic or framework-like parts of this design don't conform closely to the object/delegate pattern from ObjC.

@jemmons
Copy link
Author

jemmons commented Feb 8, 2015

@algal Another good point. I agree that, from a certain point of view, the delegate is the "real" or "actual" state machine. After all, it's the thing that determines effects and legality of transitions, and while it doesn't hold state itself, it's responsible for holding the thing that holds state. This indirection feels nominal.

The crucial function the class StateMachine performs in this pattern is the simple coordination of state transitions through its various "delegate" methods. Thus, there's an argument to be made that it's really a "StateController" or maybe "StateCoordinator" — though I'm not sure a "coordinator" is meaningfully different from a "machine". And it should be pointed out that, in UIKit, a UITableView delegates out to get cells and their effects, and we call it the "view" and its delegate the "controller".

But in the end, it's just semantics. We should pick whatever best fits our mental models.

@eastari
Copy link

eastari commented Jun 15, 2017

Hi Jemmons

@eastari
Copy link

eastari commented Jun 15, 2017

Tnx for article! Can you show example of your StateMachine ?

@eastari
Copy link

eastari commented Jun 15, 2017

Ha sorry its work
let myClass = MyClass()
myClass.machine.state
myClass.machine.state = .Fetching
myClass.machine.state = .Ready
print(myClass.machine.state)
// .Ready, .Fetching
// Fetching

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment