-
-
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 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.
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
Hi, this is very cool and an interesting approach! I had a question.
Right now, a client needs to defines four things:
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.