Today, we will discuss the last essential design element of all your modules, components, and applications.
In the previous posts, I described two essential features of a module. The first one is the interface — an outer layer of a component, which describes how other entities communicate with it. The second one is the state — data stored inside, which allows your component to work based on a specific internal context. The third element to present is the composition of a module — our current topic.
What exactly is the composition of a module. In general, it is all the logic that handles the interface and operates over the state. It is the transfer layer between different parts of the module and also the implementation of business logic. In Object Oriented programming, that may be private methods implementing the behavior of a class.
If you imagine your component in terms of fruit, the interface is the outer skin, the state is a seed inside, and the composition is all the juicy pulp in between. Bon Appetit!
There are a couple of things that could go sideways when implementing the inner logic of a module. The first one reveals itself when your component grows in size. The more logic you have, the more code you need to write and the harder it becomes to keep it straight.
How to organize the code of your module? The obvious solution is to create abstractions that could reduce the repetitions and size of the codebase. For instance, you may find similar patterns and extract a part of the implementation as shared functions. You can extract pure utility functions or create helpers — private methods in a class. You may even extract an independent part of the logic as a separate dependency module. All those things will eventually increase the readability of your code.
However, the essential thing you have to take into consideration is introducing only good abstractions. That is probably the topic for a separate article, but the rule of thumb is that having no abstraction is better than having a bad one. It is more desirable to repeat the code than to create an abstraction without a coherent implementation. For instance, when you have trouble naming or describing the scope of function’s responsibility, there is a great chance that you are about to make a mistake, and you should be very careful extracting the logic.
Another undesirable thing that could get in the way of a good design is your internal desire to implement comprehensive solutions even though some parts of it may be unnecessary.
Programmers usually have an analytical mind and prefer things that are technical, well defined, and complete. When you see a similar code, you want to unify it; when you see an incomplete logic, you want to finish it. That simply feels like just the right thing to do.
Nevertheless, those implementations and abstractions, even if they are good, may be simply unnecessary. On the one hand, that decreases code readability and, on the other hand, increases the cost of maintenance and development.
A famous description of the minimal functioning approach is known as Occam’s razor — “entities should not be multiplied without necessity”. You ought to bound yourself to do the work that is beneficial and skip the work that may be avoided. Sometimes extending an interface or introducing an abstraction may be advantageous, but remember to investigate the cost of implementation and use the razor when it seems too high.
Creating good and necessary abstractions is one thing, but handling the abundance of them is pretty different.
Sometimes extracting functions or even separate modules may not be enough. Sometimes, components and applications are so broad that you need to organize encompassed abstractions into layers.
When you have too many elements in the same space and those elements can use each other without limitations, the code will become a spaghetti sooner or later.
The best solution is to introduce restrictions and contracts that define groups into which you assign specific functions. For instance, you may have a low-level layer of logic that operates over the state, a middle-level layer that implements business logic, and an upper-level layer implementing the very logic of the interface.
Having such a separation of concerns and restrictions allows reasoning about the code more efficiently. Understanding only a part of the logic and its links is enough when you have comprehensive contracts between layers. Finally, that kind of detachment simplifies the creation of proper documentation and tests.
Careful designing the insides of your module becomes a thing when your component expands. Then, it seems natural to look for proper abstractions that are good as well as necessary.
Having a well-devised and structured code inside your module is as essential as having a good interface and a coherent state. Those three elements allow you to write your component or your app in a more anticipated way.
Basically, that’s all it is. You should design your module well to make future modifications as predictable as possible. Good luck!