Daily: Behavioral Patterns: State's the obvious.
When I was talking about behavioral patterns yesterday, I was just getting started. While the strategy pattern is great in some situations, the state pattern is useful in almost all situations where there is state.
State has been subject to quite a debate lately. The functional guys like their state immutable, the redux guys keep it global and everyone else is just using let
where var
used to be fine. or NSString *
in that regard.
But we are not talking about state in the sense used above. We consider objects and their state. Imagine, once again, an ordinary view controller. This view controllers job is to display a language selection controller (if no language has been set) and then trigger the download of some data and ultimately displaying it.
The code below pretty much speaks for itself.
class StateViewController: UIViewController {
dynamic var error : NSError?
dynamic var loading = false
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(animated: Bool) {
if (LanguageService.selectedLanguage() == nil){
launchLanguageSelection()
} else if (NewsService.hasNews() == false){
loadNews()
} else {
refreshDisplay()
}
}
func launchLanguageSelection () {
// launch a view controller
}
func loadNews () {
// do a very meaningful request and so on.
}
func refreshDisplay () {
self.tableView.reloadData()
}
}
For clarity I made up two services, one is providing the language and the other one the news. This is already ok, but personally, I don’t like the big bad if
-statement. It already has three branches, is hard to test and not what I call extensible. Also note that the code shown above does in no way handle edge cases like a failed download or the user being offline. If it would the code would become a whole lot messier. And even harder to maintain.
An easy way to improve code like that is to encapsulate a process like that – multiple, dependent steps – into several states. We start by creating a base state that every other state will extend.
class StateViewControllerBaseState {
var viewController : StateViewController?
func activate() {
// this is the base class that does nothing.
}
}
I’m using a very verbose name here to clarify the dependencies outside of a project context a bit clearar. In real world projects you might want to use shorter names to improve readability.
That’s nice. We have a state that has an optional reference back to the state view controller it belongs to. I’m using an optional here because it makes the commonly used instantiation of states a bit cleaner. We also need to add two things to the view controller: a method to change the current state and a variable that keeps the current state.
class StateViewController: UIViewController {
var state = StateViewControllerBaseState()
func updateState(state: StateViewControllerBaseState){
self.state = state
self.state.viewController = self
self.state.activate()
}
// below is the old code.
Now, let’s just think about the actual loading process. In our case, it’s rather simple: We have some initial state that’s maybe doing some housekeeping, determine the language, load the news and then display the stuff. I’ve drawn a small diagram to make it a bit more visually appealing.
So let’s create our model classes. We are writing a lot of classes here, but not so much code.
We’ll start by implementing the necessary states. The implementations are only a few lines for each state. Note that each state encapsulates only what’s necessary at this point of time and in the context of that state – how i.e. the controller handles the information is up for it to decide.
class DetermineLanguageState : StateViewControllerBaseState {
override func activate() {
if LanguageService.selectedLanguage() != nil {
self.viewController?.updateState(LoadNewsState())
} else {
self.launchLanguageViewController()
}
}
func languageDetermined () {
self.viewController?.updateState(LoadNewsState())
}
func launchLanguageViewController() {
// launch an controller that will call back to "languageDetermined"
}
}
class LoadNewsState : StateViewControllerBaseState {
override func activate() {
self.viewController?.isLoading = true
NewsService.loadNewsWithCompletion { (error) -> () in
self.viewController?.isLoading = false
if let e = error {
self.viewController?.updateState(ErrorState(error: e))
} else {
self.viewController?.updateState(DisplayNewsState())
}
}
}
}
class DisplayNewsState : StateViewControllerBaseState {
override func activate() {
self.viewController?.refreshDisplay()
}
}
class ErrorState : StateViewControllerBaseState {
let error : NSError
init(error: NSError){
self.error = error
}
override func activate() {
viewController?.error = error
}
}
With our implementations of the necessary states – ErrorState
, DisplayNewsState
, LoadNewsState
and DetermineLanguageState
in place it’s time to clean up the original view controller.
class StateViewController: UIViewController {
var state : StateViewControllerBaseState?
func updateState(state: StateViewControllerBaseState){
self.state = state
self.state?.activate()
}
dynamic var isLoading = false
dynamic var error : NSError?
func retryLoading () {
self.updateState(StateViewControllerBaseState())
}
override func viewDidAppear(animated: Bool) {
if (self.state == nil){
self.updateState(StateViewControllerBaseState())
}
}
func refreshDisplay () {
self.tableView.reloadData()
}
}
That’s a quite light view controller there. And most interesting, it doesn’t have any knowledge about how the loading works, how languages are determined or anything else. With the implementation of our states corresponding exactly to the graph above, I think this demonstrates the power of this pattern quite well. If you’ve followed closely, you’ve certainly noticed that there is still one thing missing: We are not transitioning to the first real step from our base implementation.
class StateViewControllerBaseState {
var viewController : StateViewController?
func activate() {
viewController?.updateState(DetermineLanguageState())
}
}
Once the controller appears for the first time, it now sets the initial state which in turn triggers the whole process. Each step only transitions to the next step it needs to know. Also, error states are correctly handled and the corresponding properties on the view controller are updated properly.
Just to clarify my point a bit further: Imagine a changing requirement. The available languages a user can choose from are now no longer selectable from a fixed list but rather have to be loaded from a server prior to the selection. We could just easily add a new state to implement the new requirement like so:
class LoadLanguageState : StateViewControllerBaseState {
override func activate() {
LanguageService.loadLanguagesWithCompletion { (error) -> () in
self.viewController?.isLoading = false
if let e = error {
self.viewController?.updateState(ErrorState(error: e))
} else {
self.viewController?.updateState(DetermineLanguageState())
}
}
}
}
We just swap the initial state and have the new, additional step added. Without changing a single line in the view controller or in any other state.