SOLID Design Principles: A Review of Elegant Guidelines to Robust Development
Written by Jim Ownby
At SOLTECH, our software engineers strive to offer better solutions to industry best practices and design methodologies. A question they often hear within their industry is centered around “S.O.L.I.D.” design principles. A brief overview of these concepts will help you better understand how they help software developers plan and provide best-in-class software to clients.
What is Object-Oriented Programming?
Object-oriented programming has been a mainstay of product development since the 1970s and it has roots in computer languages further back, in the 1950s and 1960s. In the last 40-plus years, there have been a variety of perspectives and guidelines that have sprung up to help project and development teams better communicate and keep focused so that product output improves over time. Every specialized vocation has its own vocabulary and tools and it can be the first mark of a crafts-person when that vocabulary is, or those tools are, properly utilized in the course of an effort.
In the culinary world, there are over 10 different types of knives utilized and it is the chef’s knife set that will stay with that chef from one restaurant to another, regardless of cuisine. In carpentry, there are over 13 different types of wood joints (or joinery) and dozens of specialized tools, allowing for countless varieties of application, as each project can have its unique challenges. Development is no different and there are dozens of development languages in use today but object-oriented languages make up a large majority of them, so it makes sense that a specialized vocabulary would evolve, and continues to evolve, over time to deal with certain challenges.
The S.O.L.I.D. Approach
One of the more recent and significant evolutions of that vocabulary involves the five design principles that make up SOLID. Individually, the utilization of these principles are not necessarily indicators of success but, together, they can make an application more adaptable and flexible to the changing needs of an enterprise or customer base. Here’s a brief and simplified detail of what they mean to a layperson.
To keep this explanation as simple as possible, I will analogize some of the workings of an application to the workings of a car. In making this effort, I have to recognize that some of my examples might stand up to initial scrutiny in comparison but could fall off rapidly, thereafter, depending on examples. Comparisons between the abstract\virtual and something concrete will eventually have this effect.
Breaking Down S.O.L.I.D.
“An object or method should have only a single responsibility”
This is likely to be the easiest of the principles to explain and yet be one of the most significant to understand. Even the simplest software application is likely to employ this principle because it can impact how the application matures over time.
From a car perspective, think of a turn-signal indicator and what it is used for. A driver uses the indicator when they are about to turn so that other drivers are aware. It usually has three specific positions and will reset once a turn has been detected, usually once the steering wheel has turned. Regardless of the conditions that govern the behavior of the indicator, though, it only has one purpose and it is up to the driver to utilize it properly. It would be unsettling to some drivers if, instead of an indicator, the turn-signal automatically activated when it sensed the turning of the steering wheel.
Likewise, in development, code should be written with this type of distinction, especially if we can confirm that it works on basic input/output. You can see this with any application that has a “Save As…” option. That function will save some output to a file, based on the file type you ask it to. This kind of singular functionality makes it significantly easier to test and make sure that it works properly. This ensures that the function does that one thing it needs to do and does it well in as many scenarios as possible.
But what if, instead, this function saved to all known file types and didn’t give you a choice? You might find that this function is less than useful most of the time and will find a different way to accomplish what you need. This speaks to the other aspect of this principle of design: code efficiency. When the function does one simple thing well, it is easier to re-utilize. The “Save” and “Save As…” can use the same function and ensure that the same behavior is observed.
The corollary to the car example would be the hazard indicator, which is normally operated via a completely different control than the turn-signal indicator but utilizes the same external indicators to communicate a completely different message to other drivers.
This principle, when properly observed, leads to a gradual increase of complexity in an application and makes the resulting code structure easier to read and maintain as it continues to mature.
“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.”
While this definition sounds complicated, it might help to cite a car example here. If the “software entity” in question here was the seats in a car, then everyone would have a certain expectation of how the seats work. That is largely what “closed to modification” means here: When you open the driver or passenger door, you expect that you will be sitting down in a seat, looking forward, and there will be a seat belt (OK, we aren’t talking vintage pre-1970s cars here.)
Open for extension, in this example, refers to the variety of implementations that car makers have on offer today: racing, bucket, loungers, captains or bench seats. There are a variety of seat coverings and some seats might have a variety of functionality, like heating, cooling or massage elements. They are still seats but their use has been extended and it is why most car manufacturers have these variations stipulated as “options”.
This guideline works well for software development, too. When software or a website is designed as a base structure with this in mind, then allowing for software adaptation later, as needs change, makes more sense and easier to employ.
“Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.”
OK, on the surface, this one might sound very similar to the previous Open/Closed Principle but it’s not. In development, you can (and want to) compartmentalize code so that you can re-use that code, as was referenced in the Single Responsibility Principle section. One means of re-use involves inheritance, where the new code becomes a subtype (or child) of the referenced code, now known as a supertype (or parent.)
This basically means that the new child code can do everything that the parent code can do and you can now add to the child code without affecting the parent. This becomes handy when you need to describe different objects that have shared qualities, like the difference between a pickup truck and a dump truck. But, because you can now modify the child code, you can also make the child very different from the parent and that’s where this principle comes into play.
Let’s go back to the turn signal indicator control as an example. In today’s cars, most of these controls have a myriad of functions added to them, depending on the make and model of car. But have you ever rented a car and tried to signal a turn, but activated the windscreen wipers instead? That would have been an example of a violation of the “correctness” (or, in this case, the behavior) that this substitution principle is trying to protect. This can be fairly worrisome to a user, if a product does not behave in an expected fashion and it’s no different for a developer.
As a software product continues to change and adapt to allow new features and functions, it becomes increasingly more important for developers to be confident that previously developed code behaves in an expected fashion. Adherence to this principle ensures this to be more likely.
“Many client-specific interfaces are better than one general-purpose interface.”
“A client should never be forced to implement an interface that it doesn’t use or clients shouldn’t be forced to depend on methods they do not use.”
The “client,” in this case, is referring to any code that references other code, be it an external library or an internal one.
Let’s go back to the pickup/dump truck example again. Depending on how you look at them, they can have very similar functionality:
- Both have a compartment at the rear for load materials.
- Both have limited seating in the driver/passenger compartment.
- Both are largely driven and controlled in the same fashion.
They also have physical aspects that they might share:
- Some pickups and dump trucks have tailgates.
- Dump trucks, and specially modified pickups have to elevate rear compartments to facilitate unloading with ease.
- Dump trucks, certain types of pickups, have multiple tires on each side of the back axle to better handle extreme loading.
- Both types of trucks can have diesel engines.
And there are attributes that they rarely share, if at all:
- Dump trucks tend to have air-enabled brakes and suspensions.
- Dump trucks tend to have poor rear visibility and are usually equipped with audible reverse indicators to help caution others of the truck’s action.
- In most places, dump trucks require a special certification to drive legally on public roads.
All of these attributes could essentially be described as an interface specification. Some of these interfaces are broader in nature, so they could apply to more than just pickups and dump trucks. Others are very specific and wouldn’t make sense if they were an available option for a family sedan or a go-kart.
The grouping that this principle is espousing involves balancing the “specific” versus the “general” and speaks, again, to how a software product matures over time. The better this balance is, the easier it is to grow and adapt a software product over the longer term, as well as improving the focus the product has over the short term.
One should “depend upon abstractions, [not] concretions.”
The high-level module must not depend on the low-level module,[ or vice verse ], but they should depend on abstractions.
This particular principle is often difficult to explain to some developers and even more difficult for some developers to put into practice. Luckily, the automotive analogies should make it a little easier here.
Almost every vehicle has at least two aspects in common:
- A driver
- An engine or motor
In most cases, the driver doesn’t even have to know how the engine or motor works to operate the vehicle. They might know a lot about the inner working of their vehicle, but this principle basically states that this should not be required. The more obvious part of this principle, the reciprocal aspect as it were, is that the engine or motor doesn’t care who the driver is to operate correctly.
When put into these terms, this all seems fairly obvious but software development is a challenging discipline to realize in these terms sometimes. This is why this principle tends to be one of the hardest to implement into a software project, especially if the project has already started. It requires a different way of thinking about how to write, develop and test code. But once this principle is embraced in a project, it makes all the other principles much more straight-forward to follow.
Over the last 60 years, software has been written in a variety of styles. It is very much analogous to how a novel is written, in its progression. Some authors will delve into great detail on the story plot and structure before they ever start writing the story and others will start writing before they even realize what the “story” is. Depending on the authors, and the developers, the outcomes can be just as varied.
In my opinion, the principles here try to implement a craftsperson quality to building software and that effort tends to start in how we communicate what we are trying to do. Not every development language or environment lends itself well to all of these principles but, luckily, each of these principles can stand on its own to provide value to a project effort and for the people who work therein. They serve as guidelines that enable a variety of experience levels and areas of expertise to better communicate and plan throughout the life cycle of a product.