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