Software organization is a difficult subject. Most developers agree that good code design is important, but few can consistently describe what good design is.
A lovely definition from author and software engineer Steve McConnell is: “Once you understand that all other technical goals in software are secondary to managing complexity, many design considerations become straightforward”. That’s a ‘wow’ definition for me. The essence of the problem that we grapple with – managing complexity.
Software projects have two types of complexity: inherent complexity in the problem being solved, and accidental complexity introduced by the development team through poor design, misunderstood specifications, time pressures or lack of skill in a technical field.
We should aim to eliminate all accidental complexity. Then what remains is only the problem that needs to be solved. In solving the problem, we should note that the human brain can only keep track of a limited number of issues at once. Think of it like mental juggling – if you have to keep too many conceptual balls in the air at once, you are more likely to drop one (i.e. create a design or coding error).
So, a key strategy for handling complexity is to break the problem down into smaller parts. In coding, we refer to methods, and the aim is to have any particular method responsible for only one behaviour (single responsibility principle). For instance, a method that stores some data into the database, should not also be responsible for updating on-screen information. Good design is definitely modular.
Additionally, methods should be ‘loosely coupled’. This means that where the method is invoked in code, it should be passed all the data that it needs to perform its function. The method shouldn’t be dependent on other methods, or even worse, on a particular programme state. When there are many inter-dependencies between methods, we are back to juggling mental balls, and need to remember, which other methods affect the piece of code we are writing, and what the next method might need from us.
Methods must be treated, as far as possible, as black boxes. We know what they do, and what data they need to do it (a clear interface), but we don’t have to know ‘how’ they do it and we especially don’t want to have to worry about any side effects they might produce.
You may have gleaned from this that the ‘way’ we design code is as important as the instructions that we write. To really improve code design, try writing code with another developer. This technique is called pair programming. It turns out, not too surprisingly, that two heads really are better than one.
Pair programming consistently delivers better, simpler code design, as well as having a built-in review process. Code produced by a pair of developers is generally of higher quality (less bugs) than code produced by a single developer.
Before you start screaming “but what about the cost of having two developers for every task – isn’t that terribly inefficient”, let me point out four big reasons why it isn’t.
Fixing code errors in testing, or even after deployment if they’ve slipped through testing, is very, very expensive. In pair programming, every line of code is being reviewed as it is written, and far fewer errors reach the testing phase.
No developer types 100% of the time. They type a bit, and then they think a bit. When two developers sit together, they are generally working at a higher pace than one, because one is thinking while the other is typing, and vice versa. On this point, developers report that pair programming is very tiring because it is so intense!
When two developers discuss their code design as they work, they naturally arrive at a better design. Just having to explain what you want to do, usually helps to clarify uncertainties. The pair will jointly produce a simpler design that is more maintainable, modular and efficient, than most developers would arrive at on their own. This is true even when one developer has a lot more experience than the other.
Pair programming is great for knowledge sharing. It is always a risk when only one developer knows a specific piece of code, and with pair programming, you always have at least two who can now support and maintain that code.
It is generally accepted that there is an overhead to pair programming if you just look at upfront development time. However, that overhead disappears entirely when you look at the full life cycle. The quality improvements result in less testing time, and a quicker delivery into production of the code.
What other issues should we consider in defining good design?
Think about how much off-the-shelf software is available. Now we know that writing software is expensive, and is usually reserved for requirements that are not standard. This inherent uniqueness adds a lot of complexity to software design. Everything that the developers are writing, is different to anything they have written before. Perhaps there are only small differences, or they could be working in completely new fields.
Developers need to interpret the good code design practices to apply them to their new situation. This makes good design an inexact science, and open to a lot of interpretation. It is one of the reasons why a great definition like McConnell’s is so important, as it helps developers to focus on the key aspects of good design, even in new situations.
Ultimately, the best design won’t help if we are solving the wrong problems. We talk about design as being “emergent” and this refers to the way that requirements tend to grow and evolve as the software is developed. The inexact science hits again – designing for change that you don’t yet know about, is like looking for the black cat in a dark room that isn’t there!
Luckily, there are some techniques that can be applied. A good design will model reality as closely as it can, making the software easier to change when the reality changes.
Separate technical aspects from business or domain concerns so that advances in technology don’t affect the entire program. These are further levels of modularization and abstraction that are part of good design.
Make your language must be “fit for purpose”. The coding language used also affects complexity. A lovely explanation of this is to consider doing long division in the days of Roman Numerals. It was an impossibly complex task, and the ancient Romans would have never believed that even children would one day do long division. Once our decimal notation was invented, the task became much simpler. So, it is with programming languages and picking the right tools for the job.
Finally, the management processes should also work for the problem being solved. Because of the very changeable nature of software development, processes like Agile can align well, and support the team in being more flexible and responsive to change. At KRS, we find a lot of benefit in Scrum, which helps to break work into small iterations (modularization). We also employ many techniques from Extreme Programming, especially pair programming.
Software craftsmanship should be actively pursued. Good design does not happen by accident, and is seldom picked up on the job without additional reading, learning, and caring about the code that is being created. Managing complexity will remain software’s primary technical imperative no matter what new technology the future holds.