SOLID Code with Emergent Design – O, I get it
This is the 3rd post on SOLID Code with Emergent Design.
"Modules should be open for extension, but closed for modification"
The idea here is that adding new features should always be able to be handled by simply adding new code. It should never require modifying existing code. The benefit is that if you can avoid modifying existing code then you can avoid inadvertently breaking existing functionality when you add your new features.
"When a single change to a program results in a cascade of changes to dependent modules, that program exhibits the undesirable attributes that we have come to associate with “bad” design. The program becomes fragile, rigid, unpredictable and unreusable. The openclosed principle attacks this in a very straightforward way. It says that you should design modules that never change. When requirements change, you extend the behavior of such modules by adding new code, not by changing old code that already works." – Robert Martin
But how do you do this? The answer is polymorphism.
For example, if you have a client that connects to a server, you may have a design like such:
Now, what happens if you want to extend your software to enable the client to connect to multiple types of servers? Using the above design, you might add a new class for our new type of server ("Server2") and then modify the existing Client class so that it can speak with Server2.
Oops, there’s that pesky modify word. Remember, the thing we’re supposed to be avoiding?
Okay, smarty pants, then just how exactly are we supposed to add support for a new server without modifying any of the existing code? The answer is to use abstraction and polymorphism, as shown below. Here, Client doesn’t know anything about the specific servers it speaks with. Instead, it simply knows how to communicate with the concept of a server (AbstractServer).
This allows us to infinitely add new servers by simply adding new Server classes that implement the AbstractServer interface without any changes to the Client class. In other words, we can extend our software to support new servers by adding code (in the form of new Server classes), without having to modify our existing code (in the Client class).
Okay, I see your point. But how can I possibly predict what will need to change in advance to create all of the necessary abstractions necessary to comply with this rule?
Well, that’s a good question. And Uncle Bob recommends that you do not try, and in keeping with agile principles, I whole heartedly concur.
The fundamental dilemma is that there is no perfect design.
- If you create abstractions for things that don’t wind up changing, you’ve wasted a lot of time.
- If you don’t create abstractions, then the code becomes more difficult to change.
So, how do you deal with this? There are two possibilities:
- Write the simplest code possible to start with. Then, only when you need the change do you put in the appropriate abstractions.
- Anticipate all possible changes and do a "crazy, big design" to "handle everything" (because we all know how well that works)
The reality, of course, is that you need to do a little of both and fall somewhere in between the two extremes. Obviously, the closer you are to #1, the more agile you will become and the faster you’ll be able to turn out working code because you’re not bogged down trying to find that elusive "perfect design."
Of course, if you follow #1 then you will need to modify code to add new features. However, Uncle Bob’s agile OOD advice for this is that when you get the requirement that necessitates the change, you refactor to meet this principle. It’s a Just In Time approach to design, and if you stay true to following it whenever there is a change, your code will always follow a sound and simple (read: easy to understand and maintain) design.