-
-
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 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.
Hi Jemmons
Tnx for article! Can you show example of your StateMachine ?
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
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.