SOLID is a system of five design principles with directives for object-oriented design intended to create more flexible, maintainable, and adaptable software. It was first introduced by Robert C. Martin in his 2002 book Agile Software Development, Principles, Patterns, and Practices (1).
The SOLID design principles cover considerations for interface implementations, class inheritance, module design, and how much responsible one object should have. These principles have evolved over decades with input from software engineers from around the world. Their insights and directives are sure to help any programmer produce better software.
The Five Principles of SOLID
The SOLID design principles focus on distinct concerns of object-oriented design. In their application, several are known to overlap in application in that violation of one is likely to violate another. While keeping in mind each, an awareness of the following five principles as a collective is ideal.
- Single Responsibility Principle (SRP)
- Open/Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
These principles have something to offer programmers of all levels of experience. These principles may take a lifetime to master but even beginners stand to benefit from a basic study of their details. Below is a brief introduction to each with some basic examples of each.
Single Responsibility Principle (SRP)
A class should have only one reason to change — Martin, 2003
The single responsibility principle is the first of the five SOLID design principles and states a class or object should bear a single responsibility within a program’s overall functionality.
As described by Martin, responsibility can be defined as an “axis of change.” Having multiple responsibilities means having multiple reasons to change which means a higher potential to break as one’s codebase grows.
A common approach at narrowing responsibility is to leverage components such as interfaces to abstract certain functionality into separate classes. For example, consider the following implementation of a stock trading application that can Buy or Sell.
By using separate classes for implementing the functionality of the
sell methods, the
Transaction class limits its responsibility thus limiting the reasons for it to change. An example of this approach is illustrated in the Single Responsibility Principle UML diagram below.
Open/Closed Principle (OCP)
Sofware entities (classes, modules, functions, etc.) should be open for extension, but closed for modification – Martin, 2003
The Open/Closed Principle (OCP) of SOLID states that programming entities such as classes, functions, or methods should be designed in ways that allow specific functionality to be added without requiring related entities to be modified. That is still a bit conceptual, so let us try to unpack it a bit further.
Consider our stock-trading application again. After making several
Sell transactions we realize the need for a unified record-keeping system. There should be a standardized format and an easily controlled set of variables such as stock, quantity, price, and probably date.
A non-OCP approach would be to add a
makeRecord method to both the
Sell classes. Each class would implement the processes for making a record of its resulting execution. What happens if the format of the records being generated needs to change? This would require modification to both the
Sell.makeRecord() methods. Such copy/paste type design is a code smell and, in this case, an opportunity to implement an alternative design based on the OCP!
To apply the Open/Closed Principle here we can add another class named
RecordableTransaction to extend via
Sell. This parent class defines a makeRecord method which is accessible via both Buy and Sell via inheritance.
This allows us to change code in the
ReordableTransaction.makeRecord() method in a way that extends the functionality of the makeRecord() method for both
Sell without modifying those classes! An example of this approach is illustrated in the Open/Closed Principle UML diagram below:
Liskov Substitution Principle (LSP)
Subtypes must be substitutable for the base types – Martin, 2003
The Liskov Substitution Principle states that any subclass object should be substitutable for the superclass object from which it is derived. This semantic relationship often called behavioral subtyping, is applied to develop more correct, extendable, and reusable software.
As an example, consider again a Stock trading application with a superclass
Transaction with methods
buy(). This class is extended by the subclass type
RecordableTransaction which has extended functionality such that all transactions generate and store a record after being placed.
The LSP dictates that any instance of
RecordableTransaction should be substitutable for an instance of
Transaction without breaking functionality. Our stock trading application design’s LSP validity is illustrated by the fact that the
RecordableTransaction has all the functionality of its superclass parent. Namely,
RecordableTransaction.sell(). Consider the Liskov Substitution Principle UML Diagram example below:
Interface Segregation Principle (ISP)
Clients should not be forced to depend on methods that they do not use – Martin, 2003
The Interface Segregation Principle (ISP) approaches the problem of interfaces with many methods for several clients, many of which do not use all the methods. This results in situations where separate clients become coupled to one another via a shared interface via methods that are not strictly being used.
The Interface Segregation Principle describes an approach to avoid cases where clients are coupled to methods they do not utilize. Consider another example of a Stock trading application with
RecordableBuyclasses to manage Actions.
Buy class executes an action without making a record whereas the
RecordableBuy class both executes an action and makes a record. This is a way to make a permanent record of a
Sell action—kind of like a receipt.
RecordableBuy from a common interface violates the ISP given that it would have to dictate
MakeRecord methods for both. The Buy class does not need to make a record of its transactions.
Implementing from an interface common to
RecordableBuy couple them to that functionality. A more ISP conforming approach is illustrated in the following Interface Segregation Principle UML example diagram:
Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules; Abstractions should not depend on details. – Martin, 2003
The Dependency Inversion Principle (DIP) approaches good design in a twofold fashion. Firstly, it dictates that high-level packages/modules shouldn’t depend on low-level modules but that both should depend on abstractions. Secondly, it states that details of software constructs should depend on abstractions, not the other way around.
These core tenants stand to shape the design of software such that high-level decisions and policy are minimally affected (ideally not at all) by low-level details of implementation. The DIP is closely coupled with other principles of SOLID design such as the SRP and Open/Closed principles.
Martin describes an ideal approach for implementing SRP as the Hollywood Method (“Don’t call us, we’ll call you”). This approach leverages the power of layering within object-oriented design without producing cumbersome dependency. Often, this is achieved by using interfaces to “bridge” higher-order packages with lower-order packages. Consider the following Dependency Inversion Principle UML Diagram example:
SOLID is attributed to Robert C. Martin (a.k.a. Uncle Bob) and first appeared collectively in Martin’s 2000 paper titled Design Principles and Design Patterns. This paper only included the Open/Closed Principle, Liskov Substitution Principle, and Dependency Inversion Principle.
The first complete survey of SOLID by Martin was noted in his 2002 book Agile Software Development, Principles, Patterns, and Practices (1). The five SOLID principles were introduced here as a means of AGILE development intended to avoid “code smell,” a term coined in a 1999 publication by Martin in collaboration with renowned computer scientist Martin Fowler (2).
The five principles of SOLID design outline a conceptual framework with applications relevant to a wide variety of software projects. These principles may come across as unnecessarily complex. In some cases, this is true—especially for small hobby projects, rapid-prototyping, or simple proof-of-concept applications.
I’m reminded of an intimidating concept every time I consider casting these principles aside in favor of a faster, more convenient workflow. These design principles have evolved over decades with the input of the smartest minds in software development. As a supplement to the principles of SOLID, I suggest reading about the SOFA principles which can help with function and method design.
Such easy access to the collective power of so many great minds is one of the reasons I fell in love with computer programming in the first place. The SOLID design principles are one such resource and can help approach many types of problems for programmers willing to take the time to learn them.
- Martin, Robert C. Agile Software Development, Principles, Patterns, and Practices. 1st ed., Pearson, 2002.
- Fowler, Martin, et al. Refactoring: Improving the Design of Existing Code. 1st ed., Addison-Wesley Professional, 1999.
- DeMarco, Tom. Structured Analysis and System Specification. 1st ed., Prentice-Hall, 1979.
- Meyer, Bertrand. Object-Oriented Software Construction. Prentice-Hall, 1994.
- Liskov, Barbara H., and Jeannette M. Wing. “A Behavioral Notion of Subtyping.” ACM Transactions on Programming Languages and Systems, vol. 16, no. 6, 1994, pp. 1811–41. Crossref, doi:10.1145/197320.197383.