-
-
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) | |
} | |
} | |
} |
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.
@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
@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.