Regardless of business requirements or platform requirements, with the continuous iteration of requirements, there will usually be a phenomenon of complicated logic and confusion, and the cost of maintenance and new functions will become very huge and unbearable. The following figure compares requirements, business code, and test code:
There are three stages in the figure:
- Stage 1: Normal, linear growth.
- Phase 2: The number of requirements grows normally, the number of lines of business code begins to grow, and the number of lines of test code increases substantially.
- Phase 3: Business lines of code start to grow dramatically, test lines jump (out of screen), and requirements start to fall.
This is a good expression of the problem of increasing complexity from the beginning of the business to the end of long iterations. The same requirement can be completed in 1 day at the beginning, but after a long iteration, it may take 3 days or more. This is not the result of the developer’s subjectivity, but the maintenance cost of the code state is too high, and in the end, it often affects the whole process. It also inhibits the speed of business iteration.
So for the long-term iteration of the product, remember not to do simply, otherwise are to the back of the hole dug.
Of course, look at the problem or look at the essence. According to the law of conservation of complexity (Tessler’s Law), every application has an inherent and irreducible complexity. This inherent complexity cannot be removed as we wish, only adjusted and balanced. At present, the complexity separation of the front-end mainly includes: framework, general components, business components and business logic, as shown in the figure below:
As you can see in the figure above, once the framework and common components are built, the complexity that can be borne is basically stable. No matter how to improve or replace other frameworks in the future, it will be difficult to break through the ceiling, and the change in business complexity will be very small (if your business has experienced the change of underlying framework, You can see if it makes a difference to your business complexity).
We need to think about where we can reduce complexity. On the other hand, can we make a breakthrough from the common “business logic” side of the business? At present, few of the business side efficiency improvement schemes are based on the perspective of “business logic”, and most of them focus on the efficiency improvement of scene.
Focusing on the “business logic” side, we look at the problems that all businesses face, and what drives business complexity. There are two main points as follows:
- The code level
- A variety of business states
flag
Variable surge: Even if you write a lot of these variables, it’s hard to know each one clearlyflag
What is it for? - All kinds of judgment of business status
if/else
:if/else
Nested hell can be seen in many large business products. And all sorts of internal logical judgments likeisA && isB || ! (isC || ! isD && isE)
I couldn’t understand it at all. Even if I asked PD, she didn’t know after a long time. There are also some bugs that you may not be aware of.
- A variety of business states
- Collaboration level
- It is difficult for business students to have a global business perspective, so it is difficult to have a say in PD’s demands. If the requirement design is not reasonable, you can only find it when you finish it in the UAT stage, and then PD will give you a new requirement for you to correct (although it is PD’s problem, there is no way to avoid PD’s mistake).
- The scope of the test, in most cases, depends on the scope of the test given by the front end student. And many times code changes, the front end is not sure exactly which pages will be affected. Therefore, it will either lead to incomplete tests of test students, or lead to the need for full regression of test students, which is a huge test cost.
- When other front-end developers are involved in projects, they have to spend a lot of money to sort out the relationship between business and code. Lead to difficulties in cooperation or handover of projects.
We need to find appropriate solutions to these problems.
1. Solve code level problems
Problems at the code level, mainly from the excessive number of Flag variables, and if/else nesting and a large number of branches, resulting in difficult to modify and expand, any change or change is fatal. In fact, there is a suitable solution to this kind of problem in the design pattern — state pattern.
1.1. State mode
The state pattern primarily addresses situations where the conditional expressions governing the state transition of an object are too complex. Complex judgment logic can be simplified by transferring the judgment logic of states to a series of classes that represent different states, reducing mutual dependence.
A state pattern is a behavior pattern that has different behaviors in different states. It decouples state and behavior.
As can be seen from the class diagram, State pattern is a perfect embodiment of polymorphism and interface orientation. State is an interface, representing the abstraction of State. ConcreteStateA and ConcreteStateB are concrete State implementation classes, representing the behavior of two states. Context’s request() method will invoke specific behavior methods of different State interface implementation classes based on State changes.
The benefit of state patterns is that they localize the behavior associated with a particular state and separate the behavior from the different states. These objects can then change independently of other objects, making it easy to add or modify the state flow in the future.
Consider using state patterns when an object’s behavior depends on its state, and it must change its behavior at run-time based on its state.
1.2. The state machine
State machine, full name finite state machine (finite-state machine, abbreviation: FSM), also known as finite state automaton (finite-state automaton, abbreviation: FSA), is a mathematical model abstracted from the operation rules of real things, rather than a real machine. State machines are a subset of Turing machines. It’s a cognitive theory. In some ways, our real world is a finite state machine.
Finite-state automata are important in many different fields, including electrical engineering, linguistics, computer science, philosophy, biology, mathematics, and logic. Finite state machine is a kind of automaton studied in automaton theory and computation theory. In computer science, finite state machines are widely used in modeling application behavior, hardware circuit system design, software engineering, compilers, network protocols, and computing and language research. It’s a very mature methodology.
Finite state machines have five important parts:
- Initial State
- A limited set of states
- A limited set of events
- A set of state transitions driven by events
- A limited set of final States
A more succinct summary, in three parts:
- State of the State
- Events in the Event
- Convert the Transition
There can only be one state at a time. For example, people have “asleep” and “awake” two states, at the same time, either “asleep” or “awake”, there can be no “half asleep” state.
In logic, things described in real life can be abstracted into propositions. The proposition is essentially the State of the State machine, and the Event is the condition of the proposition, which is deduced through the proposition and condition. And Transition is the conclusion of a proposition.
So when we get the requirements, we need to first separate out what are known propositions (states), what are conditions (events), and what are conclusions (transitions). And what we’re going to do is we’re going to use these propositions and these conditions to derive the conclusion.
1.2.1. Take the Fetch API that we often use as an example
fetch(url).then().catch()
Copy the code
A finite set of states:
Initial state:
A finite set of final states:
A limited set of events:
Idle
State only handlesFETCH
The eventPending
State only handlesRESOLVE
和REJECT
The event
A set of state transition relationships driven by events:
1.3. State machine vs. traditional coding example
Let’s use a small requirement to compare the differences.
1.3.1. Description of Requirements
Search according to the entered keywords and display the search results. As shown below:
1.3.2. Based on traditional coding
Get the result of the request according to the keyword, and then insert the result back, the code is as follows:
function onSearch(keyword) {
fetch(SEARCH_URL + "? keyword=" + keyword).then((data) = > {
this.setState({ data });
});
}
Copy the code
It may seem like a few lines of code have taken care of this requirement, but there are other issues that need to be addressed. If the interface is slow to respond, you need to give a user the expected interaction, such as Loading effects:
function onSearch(keyword) {
this.setState({
isLoading: true}); fetch(SEARCH_URL +"? keyword=" + keyword).then((data) = > {
this.setState({ data, isLoading: false });
});
}
Copy the code
A request error can also occur:
function onSearch(keyword) {
this.setState({
isLoading: true}); fetch(SEARCH_URL +"? keyword=" + keyword)
.then((data) = > {
this.setState({ data, isLoading: false });
})
.catch((e) = > {
this.setState({
isError: true}); }); }Copy the code
Of course, don’t forget to turn off Loading:
function onSearch(keyword) {
this.setState({
isLoading: true}); fetch(SEARCH_URL +"? keyword=" + keyword)
.then((data) = > {
this.setState({ data, isLoading: false });
})
.catch((e) = > {
this.setState({
isError: true.isLoading: false}); }); }Copy the code
We also need to clean up errors each time we search:
function onSearch(keyword) {
this.setState({
isLoading: true.isError: false}); fetch(SEARCH_URL +"? keyword=" + keyword)
.then((data) = > {
this.setState({ data, isLoading: false });
})
.catch((e) = > {
this.setState({
isError: true.isLoading: false}); }); }Copy the code
Is that the end of it? Have we taken all the bugs into account? Don’t. Users should not search while waiting for a search request, so do not send another request until the search results are returned:
function onSearch(keyword) {
if (this.state.isLoading) {
return;
}
this.setState({
isLoading: true.isError: false}); fetch(SEARCH_URL +"? keyword=" + keyword)
.then((data) = > {
this.setState({ data, isLoading: false });
})
.catch((e) = > {
this.setState({
isError: true.isLoading: false}); }); }Copy the code
As you can see, the complexity of applications is increasing, and you may experience scenarios far more complex than this small example. If the user wants a feature to interrupt the search because the search interface is particularly slow, a new requirement arises:
function onSearch(keyword) {
if (this.state.isLoading) {
return;
}
this.fetchAbort = new AbortController();
this.setState({
isLoading: true.isError: false}); fetch(SEARCH_URL +"? keyword=" + keyword, {
signal: this.fetchAbort.signal,
})
.then((data) = > {
this.setState({ data, isLoading: false });
})
.catch((e) = > {
this.setState({
isError: true.isLoading: false}); }); }function onCancel() {
this.fetchAbort.abort();
}
Copy the code
Special handling of a catch cannot be left behind because an interrupt request triggers a catch:
function onSearch(keyword) {
if (this.state.isLoading) {
return;
}
this.fetchAbort = new AbortController();
this.setState({
isLoading: true.isError: false}); fetch(SEARCH_URL +"? keyword=" + keyword, {
signal: this.fetchAbort.signal,
})
.then((data) = > {
this.setState({ data, isLoading: false });
})
.catch((e) = > {
if (e.name == "AbortError") {
this.setState({
isLoading: false}); }else {
this.setState({
isError: true.isLoading: false}); }}); }function onCancel() {
this.fetchAbort.abort();
}
Copy the code
Finally, there is the case where there is no value:
function onSearch(keyword) {
if (this.state.isLoading) {
return;
}
this.fetchAbort = new AbortController();
this.setState({
isLoading: true.isError: false}); fetch(SEARCH_URL +"? keyword=" + keyword, {
signal: this.fetchAbort.signal,
})
.then((data) = > {
this.setState({ data, isLoading: false });
})
.catch((e) = > {
if (
e &&
e.name == "AbortError"
) {
this.setState({
isLoading: false}); }else {
this.setState({
isError: true.isLoading: false}); }}); }function onCancel() {
if (
this.fetchAbort.abort &&
typeof this.fetchAbort.abort == "function"
) {
this.fetchAbort.abort(); }}Copy the code
For such a simple small requirement, from the first few lines of code can be completed, to the final determination of the completion of various boundaries of the code, as shown in the figure below:
As you can see, code with flag variables and nested if/else is increasingly difficult to maintain, with all the logic in your head. When you write a test, you have to go through the logic from scratch to write it.
Due to the high frequency of change in the business, many business developers do not write unit tests because of the high cost, which makes it difficult for others to understand your code when handing it over. If you write long enough, you may not be able to read the logic in the code.
This can lead to:
- It is difficult to test
- Difficult to read
- There may be hidden bugs
- Difficult to extend
- The logic is further muddled as new functionality is added
1.3.3. Based on state machines
Let’s see what we did with the state machine. Remember the flow: tease out what states there are, what events there are for each state, and what states you will transition to if you experience those events.
Here is a JSON description using the XState state machine tool:
{
initial: "Free", states: {spare time: {on: {search: 'search'}}, search: {on: {search search success: 'success' failure: 'failure', cancel: 'free'}}, success: {on: {search: {on:{search: 'search'}}},}Copy the code
Yeah, just a few lines of code describe all the relationships. And it can be visualized as shown below:
You can see that the states are expressed very clearly, and when combined with the View, there is no need to write complicated flags and if/else, the View just needs to know what state it is, and send the event to the state machine, and nothing else. In the case of new or modified requirements, it is only necessary to add or orchestrate the state.
And after visualization, there are the following changes:
- See clearly what the states are
- See clearly what events are acceptable for each state
- See clearly what state will the event be transferred to after it is received
- What does it look like to clearly see the path to a certain state
2. Solve collaboration problems
Another big problem is solving collaboration problems, which mainly include:
- Coordinate and communicate with test developers
- Coordinate and communicate with PD personnel
- Collaborative communication with other front-end developers
- Collaborative communication with users
This is where the concept of visualization comes in. Visualization is a technology that uses the perception ability of human eyes to interact with the visual expression of data to enhance cognition.
So for the most part, visualization solves a large part of the collaboration problem. Of course, you have to decide what makes sense to visualize.
To be visible, the state tool needs to be serializable. This is what a state management tool like Redux lacks, with the following main problems:
- Lack of visual ability
- States and data get mixed up
- All states are flat and cannot be described in relation to each other
2.1. A state diagram
Let’s go back to the state machine. If you use state machines to write code, the number of requirements increases, and the number of states increases, you will face the problem of “state explosion”, which is still difficult to maintain and costly to read.
Of course, this scene has been considered for a long time. In 1987, Harel published a paper to solve the problem of visualization of complex state machines, further enhanced the state machine and proposed the concept of state graph. Subsequently, from 2005 to 2015, Microsoft, IBM, HP and other companies spent 10 years to develop the specification, and launched the W3C’s State Chart XML (SCXML) specification, so far basically stable, various programming languages also based on this specification State graph packaging.
Take a look at the comparison of state machine, state graph, and handwritten code complexity, as shown below:
As can be seen from the figure:
- In traditional coding, complexity increases linearly as states and logic increase.
- With the use of state machines, the initial complexity is very low, but with the increase of states, the phenomenon of “state explosion” appears, the complexity also increases dramatically.
- With a state diagram, the upfront cost is slightly higher, but the growth of state and logic at a later stage has little impact on its complexity.
The diagram I drew for the state machine is the state diagram.
The state diagram looks something like this:
Mainly include:
- state
- State of the atom
- Composite state
- condition
- A final state
- The historical status
- The initial state
- Parallel state
- Pseudo/transient state
- conversion
- Automatic conversion
- Delay conversion
- Its transformation
- Internal transformation
- operation
- Custom operation
- Enter the operation
- Exit actions
- Data manipulation
- Log operation
- The event
- Generate an event
- Delay time
- conditions
- data
- call
Even if the state is very complex, it can be aggregated, grouped and refined by the mode of the state graph, and divided by the Actor model without “state explosion”.
2.2. The document
At present, the project requirements are mainly described as follows:
- Product Requirements Document (PRD)
- The design draft
In both cases, the description of page behavior is not detailed enough. PRD will hardly describe the interaction behavior in detail, and the design draft will probably not (because the business delivery cycle does not allow too much time to be spent on it). However, for these unclear and fuzzy points, it brings the following problems. For these details, the communication cost and laton cost between each role.
There is also a serious problem with synchronization. A lot of times during development, there are requirements changes, and most of the time these changes don’t involve reworking the PRD and the design draft, and there are problems with focusing and future retrospecting between different characters.
Both of these problems can be solved if you use a state machine. The state machine approach requires you to list all possible states before development, and the relationships between states must be clearly described. Based on the generated state diagram, you can completely express all the state interactions and changes, and it comes from the code, so it’s synchronous in real time, how your code is running, the state diagram is expressed.
2.3. Role Impact
Back to the question of collaborating with different roles. What happens with the blessing of a state diagram:
- The designer can use different states in the state diagram to determine which states are appropriate for which UI.
- For PD, you can view the status diagram to understand system behavior and verify that requirements are met.
- For both the test and the user, the state diagram serves as a complete instruction manual, revealing how to get to a state that was previously unknown.
- There is another big difference for tests. Because they are written Based on state machines, model-based Testing can be used, and these tests can be automated by some state machine tools.
- For the front-end development of handover, with manual in hand, each state is very clear, and the things that can be done are also very clear. With the state machine foundation, it can be quickly started.
2.4. Improved user experience: User operation link tracking and analysis
In addition to solving the problem of complexity, the features of state machines can also bring some new ideas, such as user operation link tracing and analysis.
2.4.1. Common analysis of user operation link methods
At present, the method of analyzing user operation link is mainly to bury points on the operable labels in the page, such as Button and Tab Item. There are manual burying point and automatic burying point.
- Manual burying point, you can collect operation data in a specific area according to your will, but the cost is high, you need to manually access one by one, and you may need to report data by yourself.
- The automatic burying point is usually automatically buried on some commonly used labels. However, specific label changes may occur, and all operable areas cannot be covered, resulting in insufficient data accuracy.
No matter which burial point is used, there is the problem of playback noise.
For example, if the report contains the operation of “View details” button, will the corresponding “Details dialog box” be displayed? At this time the link playback, can only go to guess, think that click the button, means that the dialog box out. It’s actually not accurate.
If a new feature is added to the page, determine how many users use the new feature and what they did to find it. Use this data to determine if the new interaction design is sound. It is also inaccurate in the playback of inaccurate data and “noise”.
There is a similar problem with analyzing which parts of a page are frequently used.
2.4.2. Link analysis method based on state machine
State machines do this kind of user link analysis, which is naturally appropriate. Because all operations and behaviors of users are essentially a process of “what events have been received and what state will be changed to”. This is something that is missing from the way you bury points on the View.
We just need to report the state map data to the analysis platform every time the “state” changes. The user operation link can be played back 1:1 based on state.
3. Summary
Finally, summarize the benefits and drawbacks of the state machine approach.
Advantage of 3.1.
- It’s easier to understand than traditional coding.
- Behavior-based modeling, decoupled from view.
- Easier to change behavior: The behavior in the component is extracted into the state machine, and the behavior can be changed relatively easily compared to components that embed behavior and business logic together.
- Easier to understand code.
- Easier to test
- The process of building a state map, which must explore all states, is also a process that gives you a holistic view of the business, forcing you to consider all possible scenarios.
- State graph-based code has fewer bugs than traditional code. The data shows an 80 to 90 percent reduction in errors, with the remaining errors rarely appearing in the state diagram itself.
- Helps deal with special situations that might otherwise be overlooked.
- As complexity increases, the state diagram can expand nicely.
- A status map is a great communication tool.
3.2. Some problems brought about by it
- To learn something new, a state machine is a paradigm shift, and people tend to be resistant and unwilling to step out of their comfort zone.
- The new format
- New Refactoring Techniques
- New debugging tools
- Some people don’t think visualization is useful.
- Unfamiliar coding methods can cause different resistance within the team.
- Although most people have heard of state machines, they are not familiar with them because they are far from them in actual programming.
- Programming mode conversion, many people need to figure out the original code, now how to write, how to map.
- Some will question its effectiveness.
- You have to have someone who has done it and knows it very well.
- If you’ve never used it, using this mode can be overwhelming and intimidating.
3.3. Why aren’t people using it
State machines have been developed for decades and, as mentioned above, are used in a wide variety of scenarios, such as electronics, embedded, gaming, communications, etc. Why is the front end less used?
In addition to some points in the above list of “some problems brought”, I think there are also the following problems caused by:
- Lack of tutorial books: Now search for front-end books or tutorials on state diagrams, and the results will tell you 0. There is very little data (there is a lot of embedded state machine data).
- The “do it the easiest way” mentality: Many people like to use
if/else/switch
To solve the problem. - The “You think you don’t need it” mentality: complexity in every one
flag
Variables and booleans. Just as a frog in warm water does not notice a slow rise in temperature, developers do not notice the creep of complexity. Works fine on some small systems, but gets messy as the system iterates and gets biggerif/else/switch
Statement, which modifies the state of various variables in an attempt to maintain their consistency. It’s like you don’t need a state machine until it’s too late. - Like RxJS, functional programming, everybody knows it’s good, but they just don’t use it.
3.3. Summarize
No solution will solve everything, so find the right scenario. At this point, however, the state machine is really the best tool I’ve seen for solving complex business logic.
If the problems mentioned in the article also happen around you, and can not be completely solved, it is recommended that you can try, maybe you will have a surprise.