3 Approaches to reduce complexity in software systems
Many approaches exist to handle complexity in a software system. In this article I will touch three of them: general- vs. special-purpose modules, using layers and staying consistent.
I previously wrote about information hiding in modules. In general, proper information hiding creates deep modules that have easy to understand interfaces. They hide lots of complexity behind a simple surface.
Ousterhout[1] mentions several strategies that reduce complexity in a software system. As a follow up of my last article on the topic, I would like to shortly go over three strategies for tackling complexity that resonated most with me. Try to apply them on your next project!
1 - Aim for somewhat general-purpose modules
You can distinguish modules by their scope. You can build a module that can be widely used in different scenarios. We call those general-purpose modules. You can also create modules that are very specific and only apply to one use case. We call those special-purpose modules. Let's summarize the characteristics of both.
- General-purpose modules:
- are extensible and prepare for potential future problems
- save time in the future, but increase development time now
- might as well increase mental load
- future is hard to foresee, we might not need generality
- Special-purpose modules:
- are very specific and built for the problem you have right now
- saves time today, but might increase development time later
- mental load can be lower because of focus on one use case
- refactorings likely later on when we need to adjust for more use cases
You might ask: When to use which kind of module? Ousterhout suggest the following.
Try to build somewhat general-purpose modules. The implementation should be specific for today's needs. However, the interface should not be specific. This allows for future module adaptions and reduces the impact on its consumers. The following questions can help to achieve that.
- What is the simplest interface that will cover today's needs?
- Try to reduce the number of methods without reducing overall capabilities.
- In how many situations will this method be used?
- Try to replace several special-purpose methods with a single general-purpose method.
- Is this interface easy to use for my current needs?
- Watch out for too simple and too general interfaces.
2 - Use layers to structure the system
A good idea to distribute complexity in a software systems is using layers. Indeed, many software systems apply a layered structure where each layer has a different level of abstraction. Usually higher layers have a high level of abstraction, whereas low layers have a low abstraction level. This compartmentalization helps developers to keep mental load and in turn overall complexity lower.
There are different reasons why you can find a similar abstraction level on different layers that make clear responsibility of those layers difficult.
Pass-through methods: Those are methods that do little other than passing parameters to the next method. This makes a module shallower (TK add link to modules article). Often that means that the signature of the method needs to change when the called method changes.
Interface vs. implementation: The interface of a class/module should be different from its implementation. The internal representation and interface abstraction should be different, otherwise the class is not very deep and the interface does not hide its complexity well.
Pass-through variables: This is a variable that is passed down a long chain of methods. A pass-through variable adds complexity because each intermediary needs to be aware of this variable. In turn, the introduction of a new variable forces you to touch a lot of interfaces.
3 - Consistency
I am a big defender of consistency. Not because I am (too) lazy, but because I have seen it work out very well for new joiners as well as seasoned team members. Mental load can be reduced dramatically, when similar things are done in the same or similar fashion. Less questions need to be asked. Less (historical) knowledge is necessary to understand the system.
Consistency can be applied in many different places: names, design patterns, invariants, coding style, interfaces, used libraries - to name a few. Ensuring consistency is not an easy task, especially when the project and team grows. What can help is documentation, enforcing (with tools) and following already existing (implicit) conventions while changing code. I love automating consistency as much as possible.
Conclusion
We went through three strategies to reduce complexity in a software system:
- Write somewhat general-purpose modules. The implementation should be specific for today's needs. However, the interface should not be specific.
- Use layers to structure the system and push complexity downwards.
- Stay consistent.
What approaches to tackle complexity have you used in the past? What worked well and what did not?
J. K. Ousterhout, "A philosophy of software design", Vol. 98. Palo Alto, CA, USA: Yaknyam Press, 2018. ↩︎