Every developer needs to read this: Why are most state-machine implementations tightly coupled and how do we fix them?
Every state machine implementation leads to tight coupling! Let's understand this problem in detail and find ways to fix it.
For those who haven't used state machines before: It's a design tool used to represent the application logic as a graph of interconnected nodes (or states). The above image serves as a good example for a state machine.
From the image, we have states, state-1 (in short: s1), state-2 (in short: s2), s3, s4, s5 and s6. Assume s7 does not exist yet. We'll be introducing it later.
The transitions for those states can be written as: s1 -> s2, s2->s3, s2->s4, s2->s5, s4->s6 (ignoring s7).
In a typical state machine library, each state transition will be caused by an event. And each event will cause an action to fire (the action is just a callback attached to the event). Every edge in the state machine will respond to a specific event, and each event will have one or more actions attached to them.
Thus, to transition from s1 to s2, we will have to send a specific event E(s1, s2). And to transition from s2 to s3, we will have to send another event E(s2, s3).
So now, let's say we have transitioned into the state s1: create a new blog article. The event E(s0, s1) (let's assume E(s0, s1) is same as calling "start" on the state machine) must be associated with a callback that also contains the logic to fire the event E(s1, s2).
Now, sending the event E(s1, s2), must also cause the execution of the action A(s1, s2) which is attached to it. This action must also be responsible for setting up the business logic that eventually causes either of these following events to fire: E(s2, s3), E(s2, s4) and E(s2, s5).
Therefore, setting up the conditions under which these events fire are also the responsibility of the action A(s1, s2). This causes A(s1, s2) to be aware of the events E(s2, s3), E(s2, s4) and E(s2, s5) as well, making it tightly coupled to them!
So, the state transition action that fires when the state changes from "create new blog article" to "edit blog article" must be fully aware of the existence of the states s3: "save blog article", s4: "publish blog article" and s5: "exit".
Obviously, the state transition s1->s2 will be tightly coupled with its future states as well. And this problem continues down the line, with these future states being tightly coupled to their future states!
Later, if you decide to add the state s7: "preview blog article", you will also be forced to modify A(s1,s2) to incorporate the change! This is bad! Both the states s1 and s2 should not be aware of the future states. Because it doesn't concern them. Especially because "preview blog article" has absolutely nothing to do with "create new blog article" or "edit blog article".
Let's generalize this problem:
If a state s_i has the transition: s_i -> s_j, and s_j has the transitions s_j -> s_k1, s_j-> s_k2, ...,s_j -> s_kn, then the action invoked during he transition s_i -> s_j should also include the logic that defines when the events that cause the transitions out from s_j to each of these: s_k1, s_k2, ..., s_kn are called!
Now, this makes things really complicated, as you will now have be aware of all the n transitions from s_j when you define the action for s_i to s_j!
领英推荐
Solution:
It doesn't seem natural for the action A(s1:"create blog article", s2:"edit blog article") to be aware of the states s3, s4 and s5. Because, when we have to introduce s7:"preview blog article" in the future, we will have to modify A(s1, s2).
On the contrary, it would be much cleaner if s3, s4 and s5 are independently responsible for transitioning to themselves, while A(s1, s2) stays completely unaware of them!
Every software's internals naturally gets structured hierarchically. With the entry point or the main method being at the top of this hierarchy and the more specialized functionality at the bottom.
So it is only natural that states in a state machine closer to the initial state must be less dependent on the future states and more abstract in nature.
To make this happen, let us introduce the following modification to the usual state machine structure:
Each state must implement a base class with multiple lifecycle methods. Two of which are mentioned above. We must also add lifecycle methods like onTransitionIn and onTransitionOut.
So, the conditions under which the transition s2 to s3 must happen now becomes the responsibility of the lifecycle method s3.wasTransitionedtoParent(...). This fires immediately following a transition to s2.
In the example given in the image, if we assume these additional lifecycle methods to exist, then we can move the logic of assigning the callbacks responsible for the transition s2->s3 from A(s1, s2) to the lifecycle method s3.wasTransitionedToParent(...).
Alongside this, the return transition list helps go back to a previous state. In most practical applications, we will find ourselves going back to a previous state from a future state and these return transitions make this easier.
Practically, in the state machine given in the image, A(s1: "create blog article", s2:"edit blog article") will setup the display layout for the editing environment. And it will be the responsibility of s3.wasTranstitionedToParent(...) lifecycle method to register the callbacks that will be responsible for the operation "save blog article" in the edit-blog-article's viewport.
Conclusion:
The idea of introducing lifecycle methods that fire when there is a transition into a state's parent state (and also another method to fire when there is a transition out from the parent state) allows us to design a state machine that pulls control, instead of having the control pushed to it by a previous state.
ABOUT ME: 3 weeks ago, I had decided to dedicate all my time to building a low-code ML platform that allows users to go from inception to production with just a few clicks. If you want to know more, please don't hesitate to write to me at: sreramk26 (at) gmail (dot) com