Today is the day to talk about the perfect state design. What are its main characteristics, and how to devise it for your modules, components, and applications?
This post continues the topic of modules’ design; however, it is also a standalone issue and may be read as an independent topic. If you like it, I suggest further reading about interface design.
What is the state?
Regardless of whether we talk about a component, a specific module, or the whole application, the state is an abstraction that exists in all of them. For this discussion, let’s assume that we are talking about an internal store designed solely for the purpose of an independent code.
To be more specific, the state are data that live inside your component, the local context shared by all its parts. It allows the owner to remember the current position, modify it when needed, and act accordingly based on its value.
In short, the state is all the things your module stores and acts upon.
It is vital to keep the component’s state in good condition, and one of the critical issues that make it difficult is data redundancy.
When your state encompasses data that are exact copies or are pretty similar to each other, there is a great chance that some of them are redundant, and you could throw them away, making the state smaller. Of course, there are specific cases when redundancy is intentional, like calculating intermediate data for optimization or keeping hashes for sanity checks. Still, any information that is not pure and unique is potentially excessive.
The most significant benefit of having a minimal state is that the module is easier to reason about. You can follow its logic and care only about the context that is absolutely crucial to make it fully functional. Any unnecessary addition obscures the implementation and makes it harder to devise a mental representation in your mind. It also impedes introducing future changes. Simplifying a state is like finding a fraction’s simplest form — it just feels right.
How to find those extras? Well, every data redundancy has some relation with the source element it derives itself from. When the problem is that a piece of data A implies a piece of data B, you have to trace B up to its origin — A, and recognize the relation.
There are three locations in your code where finding those unintended regularities is highly promising. The first one is the definition of your state. Explaining via comments each element of the store makes perfect sense in recognizing its importance. The second type of location are all the places where you use your state. It is a good idea to look for properties that repeatedly coexist in various conditionals. The last place to look for similarities are lines of modification of the state. Whenever you find an event that changes multiple parts of the state simultaneously, it is suitable to consider simplifying the state.
There are two common approaches to getting rid of data redundancies.
The first one is to select a particular relation you recognized, understand it well, and try to remove it by combining correlates into one. You need to name it appropriately and revise all places that reference the state.
The second one is actually a suggestion on how to design and expand a state that does not introduce unintentional repetitions. When you add features to your component, you should not allow your module’s state to be a direct result of their combination. Not every feature needs a separate part of the store. On the contrary, you should look at the state as a base representation of your component and incorporate new features without unnecessary changes. So, look at your module and the things it has to do and extend its state only when the component really needs it. The next step is to use the state you already have and implement a new feature. In this case, changing the point of view may bring tremendous advantages.
When your state contains different, unique elements, they may still relate to each other. For instance, you may have one property the value of which defines another property’s range of possible values. Everything out of the range will be considered inconsistent.
The main disadvantage in situations like this is the unpredictable behavior of your module because even though each property’s value may be valid by itself, they could also be conflicting regarding the relationship between them. That is difficult to track and control when the state grows in size.
During looking for data redundancy, you were searching for conditionals like a piece of data A implies a piece of data B. For data inconsistency, it is more subtle and may be less evident because here, you should look for associations like a piece of data A implies a range of data B (possibly B1, B2, B3, etc.). That kind of relation does not allow removing any part of the state but rather indicates potential disparities in the combination of values. Like the previous search, this one should also focus on the state declaration, its usages, and updates.
Moreover, you should keep track of your component’s logical-state. That is a set of possible states that the module could be in and is strictly related to business logic. It does not care about the component’s technical-state, which is represented by data kept inside. It is like comparing the requirements for a module with an actual implementation. The more accurately logical-state is expressed in a technical-state, the better the mental model matches the factual data representation. When you observe differences between them, that may indicate places where data inconsistency could appear.
After finding potential inconsistencies in your state, you should invest in implementing data integrity features. For instance, in comments, you can describe circumstances that do not clearly derive from the code itself. That will increase the readability of your component. You can also introduce unit tests that will validate state changes for specific cases. Covering places where irregularities may appear will enhance the quality. Another popular solution is a static code analysis, especially static type checking, which eliminates an essential category of errors during a compilation phase. When you employ different verification features, you will have a great chance to increase your module predictability.
When introducing new features or designing a new state for your component, you should remember to keep in mind the logical-state. First, analyze business requirements and develop possible states of your module. Then try to transfer this mental model of logical-state to the implementation of technical-state. This way, it is easier to correctly define and assign types to the properties of your state, resulting in a more consistent code.
An important observation is that keeping the state in good condition pays exceptionally well when your code will live for years to come. That’s why you should not compromise on the complete understanding and holistic design before introducing any changes.
Handling problems like data redundancy and data inconsistency usually starts with finding relations in your state and then addressing their effects. Sometimes, it is possible to get rid of them right away; another time, you can employ data integrity features to improve their quality.
When it comes to designing, it is better to start with the module’s responsibilities and internal logic and later concentrate on specific features. This way, you are less likely to encounter problems with your store.
Aiming for a minimal and consistent state is the best option for your component, module, and app. Good luck!