Introduction
Ever found yourself in a coding conundrum? You’ve crafted a pristine piece of software, only to watch it devolve into a digital disaster when requirements shift. If that rings a bell, you’re in the right place. Today, we’re diving deep into the art of writing adaptable, modular code that can roll with the punches.
When the requirements change… again.
The All-Too-Familiar Scenario
Picture this: I once had to design a program connecting to various network devices, each requiring its own set of configurations. And no, I’m not spilling any corporate secrets here – let’s just say I’ve made some… “creative adjustments” to the story for confidentiality’s sake.
The mission seemed straightforward:
- Connect.
- Authenticate.
- Run commands.
- Disconnect.
- Log the process (either to console or file).
But here’s the twist: Different devices, from different brands, running different OS, and using different protocols. It’s like trying to speak five languages at once!
The Challenge
While “show me active interfaces” sounds universal, it translates differently for a ZTE Router compared to a Cisco Switch. So, how do we design a system that can juggle these diverse demands without reinventing the wheel each time?
The procedural approach
One might think, “Let’s just create separate functions or classes for each vendor-device-logger combo!”
- Normally the devices from the same vendor have some similarities (the way you connect to them, the commands, and even how they conceptually think of solutions to problems.), but they are different from devices from different vendors.
- Generally, in your code, there will be some aspects you’d like to reuse. In that situation, you’d like to make a change in a single place rather than hunting down multiple places where the same logic or definition resides.
That means in our scenario we’d have something like this
<cisco router – console logging >
<cisco switch – file logging >
<ZTE router – console logging >
<ZTE switch – file logging >
You can find a code example for this approach in my Github profile example
If you need to support a new model of an existing vendor, you’ll need to implement a new function/ class (depending on your programming language of choice), and recreate all the code, handling all the corner cases differently. If you need to support a new vendor, you’ll need to implement new code for all of the devices and their models. (example)
That sounds exhausting, doesn’t it.
Inheritance approach
We can use inheritance. We know from the basic OOP principles that inheritance is the way to go when we need to reuse logic. We create a method on the parent class and whenever the logic should change, we override it.
However, there are two issues with approach
- The hierarchies aren’t always easy to identify.
- You’ll find yourself in need to start handling special cases using if statements, that’ll be repeated across the code.
Remember the Open/Closed Principle? Software should be open for extension but closed for modification. If you go down this path, you’ll see that again, with the first change, you’ll have to probably touch many existing functionalities to adapt to a new device/ vendor.
You can find an example example.
Strategy Design pattern
Fear not, for there’s a beacon of hope!Believe it or not, this is a very common problem, and smart folks have already come up with a solution.
The Strategy Design Pattern. In essence:
- Separate common behaviors.
- Prioritize composition over inheritance.
Instead of rigid hierarchies, we get flexible classes encapsulating specific behaviours. It’s like building with Lego – pick, choose, and assemble as needed.
You can find an example here
What that means
Instead of relying on having is-a relationships, we start having has-a relationships.
Instead of being tied to hierarchies and relying on overriding methods that aren’t matching the custom behavior of the child classes, we create classes that encapsulate a certain behavior.
That’ll give us the opportunity of stichting desired behaviors of objects like Lego blocks.
Now Imagine if we want to introduce a new device from Juniper that relies on telnet configurations. What we could do is immediately reuse what we have
Not only that, if we want to come up with a new kind of logging strategy, like one that sends the logs over the network, we can do that without a single modification to any existing implementation !
Check out how easy it is to add new requirements using the strategy pattern approach (here)
Design patterns
A design pattern is a reusable solution to a common problem. A time-tested answer to a frequently occurring situation.
What we have seen in this video is an example of the strategy pattern, which is only one design pattern of a family of design patterns called “Behavioural Design patterns”.
For you history nerds out there
The concept of “design patterns” as it’s widely understood today, especially in the context of software engineering, can be traced back to the book “Design Patterns: Elements of Reusable Object-Oriented Software” written by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. Collectively, these authors are often referred to as the “Gang of Four”.
However, like many great ideas in software engineering, it’s built on top of previous great ideas that come from the field of architecture.
The architect and design theorist Christopher Alexander introduced the concept in the context of architectural design. His book, “A Pattern Language,” published in 1977, described patterns as high-level solutions to common problems that occur in designing buildings and urban spaces. The notion was that these patterns could provide guidance in making design decisions, based on recurrent problems and their solutions, and could be combined and adapted in various ways to suit specific situations.
In the late 1980s and early 1990s, the idea of applying this pattern concept to software design began to take hold. The GoF book, published in 1994, popularized the concept for software engineering and provided 23 classical patterns for solving recurring design problems in object-oriented software development.
I personally find “Design Patterns: Elements of Reusable Object-Oriented Software” to be a great book, but it’s also very dry, and I’d dare to say it’s great as a reference or a cookbook but it’s a very hard read if you’re getting started. If you’re more interested in reading on the subject in a fun way,
I’d recommend you have a look at a book called “Head First Design Patterns“
Word of Caution
People (myself included) like to utilize their newly acquired knowledge, especially if it sounds cool, but what I learned from experience is, unless the requirements are super clear from the beginning and they call for a certain design pattern from the start, don’t start your software design by going all in on design patterns from the beginning. Not just requirements change, but also your understanding of your project stakeholders and the business domain.
Start simple, don’t use design patterns (and in some cases even OOP if your programming language allows that, yes I said it !) from the beginning, and once you have good command on the problem and the potential solutions, refactor your code to be more future-resilient.
Conclusion
And that’s a wrap! Thanks for joining me on this coding adventure. If you found this guide helpful, consider sharing it with fellow coders.
Stay modular, my friends. 😎