How not to shoot yourself in the foot of the state machine
The state machine is rarely used by mobile developers. Although the majority knows the principles of work and easily implements it independently. In the article we will examine what tasks are solved by the state machine using the example of iOS applications. The story is applied in nature and is devoted to the practical aspects of the work.
Under the cut, you will find an augmented transcript of Alexander Sychev's speech ( Brain89 ) at AppsConf , in which he shared the options for using a finite state machine when developing non-game applications.
About the speaker: Alexander Sychev has been developing iOS for eight years, during which time he participated in the creation of both simple applications and complex clients for social networks and the financial sector. At the moment, is a technical company in the company Sberbank. They come to programming from a multitude of spheres, having different education and experience, therefore, we first recall the basic theory. ')
Formulation of the problem
A finite state machine is a mathematical abstraction, which consists of three main elements:
multiple internal states
sets of input signals that determine the transition from the current state to the next,
sets of final states, upon transition to which the automaton terminates the operation (“allows the input word x”).
condition
By state, we mean a variable or group of variables that determine the behavior of an object. For example, in the standard iOS application “Settings ” there is the item “Boldface” (“Basic → Universal Access”). The value of this item allows you to switch between two text display options on the device display.
Sending the same “Change toggle switch ” signal, we get a different system response: either the usual typeface or bold — it's simple. Being in different states and receiving the same signal, the object reacts differently to a change of state.
Traditional tasks
In practice, programmers often encounter a finite state machine.
Gaming Applications
This is the first thing that comes to mind - as part of the game process, almost everything is determined by the current game state. So, Apple assumes the use of finite automata primarily in gaming applications (we will analyze it in detail later).
The behavior of the system when processing the same signal, but the different internal state can be illustrated by the following examples. For example:
â—Ź The game character can be of different strengths: one - in mechanical armor and with a laser gun, and the other - weakly pumped. Depending on this state, the behavior of the enemies is determined: they either attack or flee.
â—Ź the game is paused - no need to draw the current frame; the player in the menu or in the gameplay - drawing is completely different.
Text analysis
One of the popular text analysis tasks associated with the use of a finite state machine is spam filters. Let there be a set of stop words and an input sequence. It is necessary either to filter this sequence, or not to output it at all.
Formally, this is the task of finding a substring in a string. To solve it, the Knut-Morris-Pratt algorithm is used, the software implementation of which is a state machine. The state is the offset of the input sequence and the number of characters found in the pattern - a stop word.
Also, when analyzing regular expressions , state machines are often used.
Parallel request processing
A finite state machine is one of the options for implementing query processing and executing a strict set of instructions.
For example, in the nginx web server, input requests for various protocols are processed using state machines. Depending on the specific protocol, a specific implementation of the state machine is selected, and accordingly, a known set of instructions is executed.
In general, there are two classes of tasks:
managing the logic of a complex object with a complex internal state,
formation of control and data flows (algorithm description).
Obviously, such common tasks are encountered in the practice of any programmer. Therefore, the use of a finite state machine is possible, including in non-game, content applications that most mobile developers are involved in.
Next, we analyze where and when the state machine can be used to create typical iOS applications.
Most mobile applications have a layered architecture. There are three base layers.
Presentation layer.
Business logic (Business logic layer).
A set of helpers, network clients, and so on (Core layer).
As stated above, the state machine controls objects with complex behavior, i.e. with a complex condition. Such objects are precisely in the presentation layer, because it makes decisions processing user input or messages from the operating system. Let's look at different approaches to its execution.
In the classical architectural metaphor Model-View-Controller, the state will be in the controller: it decides what is displayed in the View and how it reacts to the input signals: pressing a button, changing the slider, and so on. It is logical that one of the implementation options for the controller is a finite state machine.
In the VIPER state is in the presenter: it is he who determines the specific navigation transition from the current screen and the display of data in the View.
In the Model-View-ViewModel, the state is in the ViewModel. Regardless of whether we have reactive binders or not, the behavior of the module defined in the MVVM metaphor will be recorded in the ViewModel. Obviously, its implementation through a finite state machine is a valid option.
On the business logic layer of the application, there are also complex objects with a non-trivial set of states. For example, a network client that, depending on whether or not a connection to the server is established, sends or blocks requests. Or an object to work with a database that needs to translate language functions into an SQL query, execute it, get an answer, translate into objects, etc.
In more specific tasks, such as a payment module, in which a set of states is wider, complex logic, the use of a finite state machine is also correct.
As a result, we find that in mobile applications there is a multitude of objects, the state and logic of which behavior are described more difficult than with one sentence. They need to be able to manage.
Consider a real example and understand at what point the finite state machine is really necessary, and where its use is not justified.
Consider ViewController from the Championship Championship iOS application, a popular sports resource. This controller displays a set of comments in a table form. Users enter the match description, view photos, read the news and leave their comments. The screen is quite simple: the underlying layer sends the data, they are processed and displayed on the screen.
Either real data or an error can be transferred to the display. This is how the first conditional operator appears, the first branch, which determines the further behavior of the application.
The next question is what to do if there is no data. Is this condition an error? Most likely not: not every news has user comments. For example, hockey in Egypt is of little interest to anyone, in such an article there are usually no comments. This is the normal behavior and the normal state of the screen that you need to be able to display. This is how the second conditional operator appears.
It is logical to assume that there is also a starting state in which the user is awaiting data (for example, when the comment screen is only displayed on the screen). In this case, correctly display the download indicator. This is the third conditional statement.
So it turns out already four states on one simple screen, the logic of display of which is described through the if-else-if-else construct.
And what if there are more such states? Iterative development of the screen leads to a tangled coil of conditional constructions, heaps of flags, or a cumbersome multiple switch-case. This code is scary. Imagine that the developer who will support him knows where you live and he has a chainsaw, which he always carries with him. And you so want to live to your small, but well-deserved retirement.
I think in this case it is worth considering whether to leave such an implementation in the application.
disadvantages
Let's understand what we don't like in this code.
First of all, it's hard to read .
Once the code is poorly read, it means that the new developer will find it difficult to figure out what exactly is implemented in a particular project location. Accordingly, he will spend a lot of time analyzing the logic of the behavior of the application - the cost of support and development increases .
This code is inflexible . If you need to add a new state that does not follow from the current ladder, it may not be possible at all! If you need a through transition - abruptly quit passing checks on this ladder - how to do it? Practically nothing.
Also, with this approach, there is no protection against dummy states . When transitions are described via switch case, the default behavior is most likely implemented. This state is logical from the point of view of program behavior, but it is hardly logical from the point of view of human or business logic of the application.
What could be the solution to the indicated shortcomings? Of course, this is the construction of the logic of each module / controller / complex object, not based on intuition, but using a good formalized approach. For example, the state machine.
Gameplaykit
As an example, for the implementation of take what Apple offers. Within the framework of the GameplayKit framework, there are two classes that help us work with the state machine.
GKState.
GKStateMachine.
By the name of the framework, it is clear that Apple wanted to be used in games. But in non-game applications, it will be useful.
The GKState class defines a state. To describe it you need to perform simple steps. Inherit from this class, set the name of the state and define three methods.
isValidNextState - whether the current state is valid, based on the previous one.
didEnterFrom - actions on transition to this state.
willExitTo - actions when exiting this state.
GKStateMachine is a finite state machine class. It's even easier. It is enough to perform two actions.
We transfer a set of input states to a typed array through an initializer.
Perform transitions depending on the input signals using the enter method. The initial state is also set through it.
It can be confusing that any class is passed as an argument to the enter method. But it should be noted that an object of any class cannot be specified in an array of states — strict typing prohibits it. Accordingly, if you set an arbitrary class as the class of the next state, nothing will happen, and the enter method will return false.
States and transitions between them
Acquainted with the framework from Apple, back to the example. It is necessary to describe the states and transitions between them. This should be done in the most understandable form. There are two common options: a table or in the form of a transition graph. Graph transitions, in my opinion, a more understandable option. It is in the UML in the standardized version. Therefore, we choose it.
In the transition graph there are states, which are described by names, and arrows, by which these states are connected to describe transitions. In the example, there is an initial state — we expect data — and there are three states that can be reached from the initial state: data is received, there is no data, and an error.
In the implementation we get four small classes.
Let us examine the "Waiting data" state. At the entrance it is worth displaying the loading indicator. And when you exit this state , hide it. To do this, you need to have a weak reference to the ViewController, which is controlled by the state machine you are creating.
Machine parameters
The second step that needs to be done is to set the state machine parameters. To do this, create states and transfer them to the object of the automaton.
Also be sure to set the initial state
In principle, everything is automatic. Now it is necessary to handle reactions to external events, changing the state of the machine.
Recall the problem statement. We got a ladder from the if-else, on the basis of which the decision was made, what action should be performed. As a simple machine control, such an implementation may be (in fact, a simple switch is a primitive implementation of a finite state machine), but we practically do not get rid of the previously mentioned drawbacks.
There is another approach that will allow you to get away from these ladders. He proposed the classics of programming - the so-called "gang of four."
There is a special design pattern, which is called “State”.
This is a behavioral pattern, similar to a strategy, that describes the abstraction of a finite state machine. It allows the object to change its behavior depending on the state. The main purpose of the application is to encapsulate the behavior and data associated with a particular state in a separate class. Thus, the state machine, which initially made the decision on which state to call, will now transmit the signal, transmit it to the state, and the state will make the decision. So partially unload the ladder, and the code will become more pleasant to use.
Standard framework does not know how. He assumes that GKStateMachine will make a decision. Therefore, we will expand the final automaton method, where, as a configuration, we will transfer the description of all conditional variables that uniquely determine the next state. Inside this method, you can delegate the selection of the next state to the current state.
Good practice is to describe the state as a single object and always pass it on, rather than writing many, many input parameters. Next we delegate the selection of the next state to the current one. That's the whole upgrade.
Advantages of GameplayKit.
Standard Library. Do not download anything, use cocoapods or carthage.
The library is quite simple to learn.
There are two implementations at once: on Objective-C and on Swift.
Disadvantages:
State and transition implementations are closely related. The principle of sole responsibility is violated: the state knows where it goes and how.
Duplicate states are not controlled at all. An array is passed to the state machine, not a multitude of states. If you pass several identical states - the last one from the list will be used.
What else are the options for the implementation of the state machine? Let's take a look at github.
Implementations on Objective-C
TransitionKit
This is the most popular, long-existing library on Objective-C, devoid of the shortcomings identified by GamePlayKit. It allows us to implement a state machine and all actions associated with it on the blocks.
The state is separated from the transitions .
There are 2 classes within TransitionKit.
TKState - to set the state and input, output actions.
TKEvent - class to describe the transition. TKEvent binds one condition to another. The event itself is simply a string.
In addition, there are additional benefits.
You can transfer useful data during the transition . This works the same as using NSNotificationCenter. All useful payload comes in the form of userInfo dictionary, and the user himself parses the information.
Wrong transition has a description . When trying to perform a non-existent - impossible transition - we get not only the NO value when returning from the transition method, but also a detailed description of the error, which is useful when debugging a finite state machine.
TransitionKit is used in the popular network combine RestKit. This is a pretty graphic example of how a finite state machine can be used in the core of an application when implementing network operations.
RestKit has a special class - RKOperationStateMachine - for managing concurrent operations. At the input, it accepts the process being processed and a queue for its execution.
Internally, the state machine is very simple: three states (ready, executed, completed) and two transitions: start and end execution. After the start of processing (and for any transitions), the state machine launches the control of a user-defined block of code in the queue specified when creating it.
The operation associated with its automaton transmits external events to the automaton, and it performs state transitions and all related actions. State machine takes care of
Asynchronous code execution
atomicity of code execution at transitions,
control the correctness of transitions
cancel operations
correctness of change of operation state variables: isReady, isExecuting, isFinished.
Shift
In addition to TransitionKit, it is worth mentioning separately Shift , a tiny library implemented as a category above NSObject. This approach allows you to turn any object into a state machine, describing its state in the form of string constants and actions in blocks at transitions. Of course, this is more an educational project, but quite interesting and allows you to try what a state machine is with minimal cost.
Implementations on Swift
There are many finite state machine implementations on Swift. I will single out one ( remark : unfortunately, the last two years after the report, the project has not developed, but the ideas embodied in it are worth telling in the article).
SwiftyStateMachine
In SwiftyStateMachine, a finite state machine is represented by a non-mutable structure; through the didSet methods, the property can easily catch state changes.
In this library, a finite state machine is defined through a table of correspondences of states and transitions between them. This scheme is described separately from the object that the machine will control. This is implemented through the nested switch-case.
Key features, advantages of this library are.
The need to fully describe the state transition scheme. This makes it possible to get an error at the compilation stage if the transition for a particular state is not processed.
Strict control of input signals. You cannot pass to a state machine a signal that is not defined or that is defined for another state machine.
Separating the circuit and the object it controls saves time on initializing the machine.
Visualization using graph description language DOT. There is a graphical markup language for working with state diagrams - DOT. This library uses it to indicate how the state machine will be rendered.
Conclusion
Let's note the main advantages of using a finite state machine in mobile applications.
Formalization. When describing a task through a finite state machine, it is necessary to think about all the states in which the object may be. So we get the documentation, and we can identify moments not considered in the problem statement. Accordingly, code testing is simplified.
Control data streams. Explicit call sequence control (flow of control).
Error control. If the finite state machine falls into an erroneous state, then this simply means that during the design we forgot to define another state.
Single entry point for logging and statistics collection. For example, SwiftyStateMachine allows you to explicitly specify a specific block in which you can pledge what happens to our data. This greatly simplifies debugging applications.
Operations history. Using a finite state machine, you can implement undo operations. Or, on the contrary, to restore the entire picture of transitions between states. The stack of operations is usually stored in the state itself.
Now let's look at some real examples of the use of a finite state machine.
A state machine is often used to control the algorithm. An excellent example is a taxi order that contains a large number of states. If you do them roughly, intuitively, you get a big switch case: wait for the car, the car arrived, payment - everything will not fit into the slide.
Let's formalize. There is an order placement status. The user is waiting for his confirmation, may cancel. The driver arrives, the user goes on a journey, makes a payment, then the order is moved to history. After the order is evaluated.
We now describe the states in compact classes that can be distributed among the development team, and each of them will be tested. This parallels and simplifies the work.
Another similar task is to place an order . In the application for the store, you can formalize the order: his journey from the basket to the consumer - using the state machine.
In the application "Poster-Restaurants" the state machine is used to pay for the order .
By the way, it is not necessary to describe the transition diagram. To formalize the tasks, it is enough to get all the screens from the designer.
Another option for using the state machine is the app coordinators - this is a set of instructions, a set of sequences of hard-coded actions that completely describe the user history: authorization, order execution, and so on. The management of this story can be delegated to a specific superobject, which will determine the rules within this story.
If you look closely, the app coordinator looks like a state machine. It has a set of signals and transitions between them according to given states. It is logical that if you implement app coordinators as a state machine, you can reduce all application transitions to a hierarchy of finite automata, and thereby completely formalize the task . The introduction of additional abstraction increases the amount of code, respectively, increases the development time, but this code will be fully tested and formalized. These advantages are very appealing.
So, the state machine should be used when formalization is really needed, the code is highly testable, and you need to distribute tasks among developers.
Do not try to use the state machine for objects that have one if-else. This is a bad practice, and it does not help with the development of your application.
This year, at Apps Conf 2018, which will be held on October 8 and 9, Alexander plans to discuss the five basic principles of object-oriented programming and the limits of their applicability.
For more reports on mobile development, see our YouTube channel . And if you want to receive information about new transcripts and interesting reports, subscribe to the thematic newsletter .