Modularization
Introduction
What does it mean for a piece of software to be well designed? The exact meaning of "well designed" will depend on the context, and if you ask ten different software engineers you will probably get ten different answers. Some of the most common answers might be that well designed software is easy to understand, easy to change, and easy to test. Another might point to properties like abstraction, encapsulation, loose coupling, and separation of concerns. All of these answers are correct, but modularity is the key to achieving all of these properties.
Any system is easier to understand if you can focus on one part of the system at a time. It is easier to change if the parts of the system is clearly isolated from each other so that you can make a change to one part of the system without needing to change the entire system. A system is more testable if you can test one part of the system in isolation. Abstraction, encapsulation, loose coupling, and separation of concerns are all techniques to achieve better modularity.
Modularity has always been considered a hallmark of good software design, but I would argue that it is the most important aspect of software design. No system that is not modular can be considered well designed. If you are not already thinking about modularity when you are designing your systems, you should start.
What is Modularity?
Recognizing a Modular System
Modularity applies at all levels of abstraction. Individual functions are modules inside a class, classes are modules inside a package, packages are modules inside a system, and systems are modules inside an ecosystem. The principles of modularity are the same at all levels, but the techniques we use to achieve modularity might differ.
When you are first looking at a system, it can be difficult to tell how effectively it has been divided into modules. Here are some questions you can ask to help you recognize a modular system:
- Is there some idea of an inside and an outside that is controlled through a well-defined interface?
- Can you understand a component without needing to understand the entire system?
- Can you change a component without needing to change the entire system?
- Can you test each component in isolation?
- Is the scope of variables and functions limited to prevent misuse?
Benefits of Modularity
Understandability
One of the biggest impact good modularity will have on your code is the effect it will have on your cognitive load. Modular blocks of code can be understood in isolation, limiting the amount of code you need to keep in your head at any one time. This makes it easier to understand the code, and it makes it easier to reason about the code.
This results in code that can be changed more safely, because your understanding of the code is more likely to be correct. If you do end up making a mistake, that mistake will be easier to find and fix.
Code Reuse
Breaking your system into general purpose modules will make it easier to reuse those modules. If some useful piece of functionality is spread throughout your system, it will be difficult to reuse that functionality in another system. If that functionality is isolated in a module on the other hand, it will be easy to extract that module and reuse it in another system.
For small modules it might be as simple as copying and pasting the module into the new system without modification. In other cases you might want to extract the functionality into a library or microservice that can be included in other systems. This will make it easier to maintain the functionality, because you will only need to make changes in one place.
Even reuse inside of the same system is made easier by modularity. If you have a piece of functionality that is used in multiple places in your system, you can extract that functionality into a module and reuse it. Unless your system is cleanly divided into modules, this sort of reuse will be difficult or impossible. You will end up duplicating code, which will make your system harder to maintain and more error-prone.
Costs of Modularity
Building a modular system will require more initial effort than building a more monolithic system. You will need to spend time thinking about how to divide the system into modules, and you will need to write more code to define the interfaces between those modules. However, the long term benefits of modularity will usually outweigh any short term costs.
Optimizing your development style to minimize the amount of typing you have to do is entirely the wrong approach. You will spend far more time reading and maintaining code than writing it, so you should optimize for understandability, not writability. Modularity is one of the best ways to make your code more robust and maintainable.
Modularity at Different Levels
Modularity exists, and can benefit, us at all levels of abstraction.
- Between the hardware and the operating system
- Between the operating system and the application
- Between components of the application
- Between out system and other systems in the ecosystem
These separations simplify the system, limiting the scope of changes and the cognitive load required to understand the system. These layers of abstraction greatly simplify programming, and make it possible to build systems that are far more complex than what we could accomplish without them.
Testability and Modularity
It is easier to write automated tests for a modular system than for a monolithic system. This is because you can test each module in isolation, which means that you only need to write tests for the inputs and outputs of the module, rather than for all possible paths through the code. This greatly reduces the number of tests you need to write, and it makes it much easier to understand what went wrong when a test fails.
If you are finding it difficult to write tests for a part of your system, that is a good sign that the part of the system is not modular enough. Tight coupling between modules makes it difficult to test those modules in isolation. Look for ways to isolate the part of the system that you are trying to test, so that your tests can be more focused and more reliable.
It is only sensible to try to make the process of writing tests as easy as possible, and simply by making the testing process easier, you will be making your system more modular. Focussing on testability is one of the easiest and most effective ways to improve the modularity of your system.
Services and Modularity
Keeping modularity in mind can also help when designing services. Services should be designed to be as modular as possible, so that they can be easily reused and tested. This means that services should have well-defined interfaces, and they should be designed to be as independent as possible. This will make the system simpler and easier to build. It will also allow the team to be more independent, because they will be able to make changes to their part of the system without needing to coordinate with other teams. This leads to faster development and more reliable software.
When designing a service, you should think about what parts of the service can be isolated and reused. If you are breaking a system down into services, you should focus on isolating the various parts of the system from each other. This will make it easier to change the system, because you will be able to make changes to one part of the system without needing to change the entire system.
Microservices
Microservices are a way of designing services that emphasizes modularity. Each microservice is a separate module that can be developed, deployed, and scaled independently. This makes it easier to change the system, because you can update, or even replace, one microservice without needing to change the entire system.
A good microservice architecture will be:
- Independently deployable
- Solves a single business problem
- Is owned by a single team
Many organizations are not careful enough about how they divide their systems into microservices. They end up with microservices that are too tightly coupled and can't be developed independently. If two systems need to be built or deployed together, then they are not really separate microservices. Even worse, sometimes ownership of a microservice is split between multiple teams, which just leads to confusion and conflict.
Organizational Modularity
A modular approach can also be applied at the organizational level. This means that you should try to divide your organization into teams that are as independent as possible. Each team should be responsible for a separate part of the system, and they should be able to make changes to that part of the system without needing to coordinate with other teams. This gives the team a sense of ownership over their part of the system, and it allows them to make changes more quickly and more safely.
It used to be common that major changes to a system would need to be approved by a Change Approval Board (CAB). This was a way of ensuring that changes were not made that would break the system. However, studies have shown that adding this sort of step to the development process actually makes the system less reliable. It increases the amount of time it takes to make changes, and it increases the chance that changes will be made incorrectly.
A CAB only results in creating worse software, and doing so more slowly. Teams need to feel ownership of their part of the system, and they need to be able to make changes to that part of the system without needing to get approval from other teams in order to be as productive as possible.
Organizational Change
Having independent teams also means that shifts in organizational priorities, processes, or culture can be handled more easily. An incremental change in one part of the organization will not require a complete overhaul of the entire organization. This makes it easier to adapt to changes in the market, changes in technology, or changes in the organization itself.
Conclusion
Modularity is the key to managing complexity in software design. While having a modular system does not guarantee that the system will be well designed, it will almost certainly be better than a system that is not modular. Modularity is the key to understanding complex systems, and it is the key to being able to change those systems safely. If you are not already thinking about modularity when you are designing your systems, you should start.
Many existing systems have been allowed to grow organically, without much thought given to modularity. These systems can be horrible and frustrating to work with. When in that situation it can be tempting to just add code wherever it will work, but that will only make the problem worse. Instead, you should try to find parts of the system that can be isolated and refactored into modules. This will make the system easier to understand and easier to change.