Programming is an art of finding the right balance between making software insufficiently flexible and overly complicated. If your code is of poor quality it is usually not easy to change (under-design). While, if your code is too flexible it is usually too complex (over-design). How to find the right balance?
Very often programmers, who read relevant publications on patterns and principles (such as Martin Fowler’s or Gang of Four) start using them in practice to increase the quality of their code. This is of course a very good sign – it means that they care. The potential issue that appears in some of these cases is that they use them too often and protect themselves against any potential danger that might come in the future. This introduces a higher complexity of their code and usually results in lower readability and more time required to introduce new pieces of the code. Solution to make your code just as complex as needed at a certain moment, but still adequately flexible is to use an approach of just-in-time design. There is no formal definition of this term, however shortly speaking, it means that you provide a certain design element when it is really needed. Ha! I’m sure some of you will completely disagree with that. And of course those who do are partially right – sometimes we think we already know that something will be needed sometime in the future, so why not provide an appropriate design for this now? Well the truth is that we live in a very rapidly changing world and the same applies to software. We cannot predict the future, so we should focus on well-know and current requirements instead of thinking of the ones that probably will emerge in the long-term future. To summarize: just-in-time design means that we emerge our design based on what we know and when we know it, not based on rough prediction. Enough of theory, let’s move to some examples to make you more aware of how to apply this in some real-world scenarios.
One of the most well-known software principles is the one called Open-Closed:
“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.”
The above means that these entities should allow their behavior to be modified, but without altering their code. Complying with these rules considerably minimizes changes to existing code and the risk of breaking it when adding new functionalities. There are many ways of complying with this principle, the most obvious ones are through inheritance or polymorphism. Let’s say that we have initially class A with two public methods: Method1 and Method2. Then, somewhere in the future we suddenly need to change some aspects of the behavior of Method2. We could do this directly in class A (by e.g. adding some “if” statement in Method2 of this class) which will break the rule of the class being “closed for modifications”. A better solution would be to create a class B that will inherit from class A and make a change in the overridden version of Method2 (see the picture below).
The class B will now contain only what is different and new and will, thanks to this, be simpler and safer to work with. This example is called “Open-Closed through direct inheritance” and is meant to increase the reusability by means of inheritance. There is also another solution to this challenge. Let’s come back to the initial design and assume that the Method2 of class A uses Method2_Service_Version1 which inherits from abstract class Method2_Service to accomplish its behavior:
} Now, when changing the behavior of Method2 we can add a new class Method2_Service_Version2 where a new behavior will be placed. This will make no changes to the existing code in class A and in Method2_Service_Version1 and therefore we have complied with Open-Closed through polymorphism.
So: should we apply Open-Closed wherever we see a chance to increase reusability and flexibility? Well, not really – we should not forget that applying such patterns as O/C also increases the complexity of our code. Then, when should we apply it? The answer is simple: just in time.
Refactor to Open-Closed
Let’s consider the following example: We want to create a very simple game. Let’s call it World of Tanks (assuming it is not existing yet on the market). Firstly, we need to validate it with the end users to check whether there is any potential for such game to become popular. As the first step we need to design the Tank. We assume that in the first version of the game there will be just one type of tanks, with one weapon, etc. We could do this in the following way:
But then you could say it is not complying with Open-Closed and is not flexible. Okay, let’s make it Open-Closed through polymorphism:
We can say that O/C has been appropriately applied and we have improved the flexibility of our code – now we just need to wait for the changes to take advantage of our work. The only problem is that none of the possible variations taken into account when designing is varying at the moment, whereas we have already a design that is far more complex than we indeed need. Accommodating the current design to all possible variations will usually tend to produce designs that are overly complex. Of course you can say that there are many things that are not varying at the moment but are likely to vary sometime in the future. The problem is that it is just a prediction and you never know which of your predictions will come true.
To face the above challenges a term called “refactor to Open-Closed” has been introduced. Refactoring to the open-closed allows us to introduce design elements as they are needed but not before. Thanks to this we can proceed with “just-in-time” based on what we know and when we know it instead of on pure prediction.
Let’s say we have coded our solution according to the initial design which is not complying with Open-Closed. Sometime in the future a new requirement arises: “We need to provide our tanks with a new type of a weapon“. We need to accommodate the Fire method to adjust it to this requirement. We could put these changes directly inside this method, but this is not the way to go. It’s definitely better to extract a class as shown on the picture below.
Then we should extract interface:
And finally add a new class that will implement the extracted interface and will cover the new requirement:
The behavior of Tank has not been changed. We have performed a just-in-time refactor with the compliance to Open-Closed principle.
Remember: if you try to design for every possible future change it might lead to an overdesign. Try to make your decisions on what actually happens (empiricism) and not on what you suspect might happen (prediction). Introduce elements as you need them – use refactor discipline to accomplish this goal. Fortunately, our IDEs are enabling quite advanced automated refactoring operations nowadays, which makes it not such a hard task as it used to be in the past (when the Open-Closed principle has been invented). What is more, if you know how to refactor efficiently (I recommend to read Martin Fowler’s book “Refactoring – Improving the Design of Existing Code” ) it should be quite easy for you to introduce changes in your code. For those who would like to read more about just-in-time design and refactoring to Open-Closed I recommend a book Essential Skills for Agile Developer. The whole subject of “finding the right balance” in the context of software design is always quite controversial. There will always be people who prefer to make some upfront design “hedges” because their intuition already tells them that they will need it. And it’s not necessarily a bad approach. However, what we must always remember about, is that we are not able to predict everything and whatever we’re trying to predict now still remains just a “might-or-might-not-happen” scenario. I am curious about your opinion regarding the presented approach to programming – feel free to leave a comment below!