Software Design is not Product Design
I’ve talked about one aspect of this topic before, where I discussed the common argument that software design takes too much time to be of any benefit. Software design, architecture and design patterns; those seem to be viewed as some sort of profanity by some developers and other business people. This is a quite common view of it. The reason why that is, is sort of a mystery, but I think I get the point.
Don’t compare software design to traditional design
Whenever we mention design, we may think about the process of designing a new physical product that will get mass-produced. Whenever we mention architecture, we tend to think about the process of designing buildings. So as soon as software design is mentioned, some people may have this idea in their head that the design process is the same for software. It is not. The design and the implementation are two completely different processes in traditional architecture. In software, however, those two are intertwined.
When a new building is to be built somewhere at some location, a detailed plan of how this building is to be constructed is needed. That’s where traditional architecture has its use. We must know whether this building actually can be built, whether it’s actually useful, whether its location is appropriate and whether it interferes with the surroundings. We need a detailed plan for this, because changing a building in mid- or post-construction can be very expensive and even impossible at times. Changing software during those times, however, is much cheaper. We should use this opportunity to defer design choices as long as we can.
However, there are still two activities that need to be separated when writing code. I’d like to think of each of those activities as proactive and reactive activities.
The proactive part of software design
In order to build great software, you need to know who’s going to use it, why they’re going to use it and how they will use it. The proactive part of software design is the analysis phase. We may know what’s needed, but we don’t know what problems to solve yet. Therefore, we need to talk to the person who’s asking for a certain feature to get a better understanding of their view of the problem that needs to be solved. Then, if you’re writing in an object-oriented language for example, you could come up with an object model that describes the problem with objects. This model should capture the major concepts that make up the problem and how those concepts relate to each other. The person who’s asking for the feature should be able to understand the object model, because then you can actually try to solve the problem together with the end user. If they don’t understand something in the model, they’ll tell you so, and you can change the model to better reflect their view of the problem. If you’ve read the book Domain Driven Design by Eric Evans, you may know that this process is what Eric Evans call knowledge crunching. This model can provide loads of benefits, and Domain Driven Design is all about this model, which is also known as the domain model.
Once you’ve gotten this model right enough for the moment, you’ll jump right into coding and the reactive phase begins.
The reactive part of software design
The object model shouldn’t be a detailed plan over how the implementation is going to work. The object model should, however, be the soul of the implementation. The object model captures major concepts and the important relationships between those, as well as the activities involving them. This model should appear in some way in the code; the code should use the same language and relationships between the concepts (this is very important in particular) that this model captures. Each object in the model doesn’t necessarily correspond to one class. It could be a class, a collection of collaborating classes or a family of classes, or maybe even a process. Those decisions are implementation details, and should be deferred until a decision actually needs to be made.
What I tend to do, is to try to write as simple code as possible at first. As an example, I may start with a simple concrete class and develop it in a test-driven fashion. Then as I add more code to this class, I might get a feeling that it is growing too big, or that there are several concepts that are represented in this one class. Sometimes the obvious choice is to move some of the code to a new class which might help communicate the problem being solved better, or just be more maintainable overall. However, there are times where I feel I am oscillating between two design choices and I can’t really choose which one to go for. This is usually a sign that you’re thinking one step too far ahead. Instead, continue writing code, even if it may look rubbish for the moment, and then make a decision when the problems start to become much more apparent. Who knows, the design deficiencies you anticipated may turn out to not appear at all, and the current design may just be enough.
When some piece of code in the codebase starts to grow in complexity, it is usually a sign of an insufficient modelling of the problem. This should be addressed as soon as possible by jumping into the proactive phase and then refactoring the code to better reflect the new understanding of the problem. We will probably never get it quite right the first time, anyway. This is also the problem that technical debt attempts to describe in terms of money.
Summary
It is important to understand the problem you’re going to solve. You need to understand what concepts are at play and how those relate to each other. It is important to get the same view of the problem as the end user, because when they ask for new changes or features they usually think about the changes in the context of their own mental model of the problem. Those changes they ask for may be completely reasonable in their mental model, but if the code represents a whole different model, those changes may not be as reasonable in the code. The smaller the gap is between the code’s underlying model and the end user’s mental model of the problem, the easier it will be to change the design and add more code in the future. The code will live in harmony with the domain. But the road to closing this gap is rough, and so trying to better understand the problem domain as you go is key.
If you’re interested in this topic, I urge you to check out the book on Domain Driven Design by Eric Evans.