SOLID: Guidelines for Better Software Development

solid object oriented design princples overcoded

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.

  1. Single Responsibility Principle (SRP)
  2. Open/Closed Principle (OCP)
  3. Liskov Substitution Principle (LSP)
  4. Interface Segregation Principle (ISP)
  5. 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 buy and 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.

sinlge responsibility principle srp uml diagram example overcoded
Factoring out the sell and buy methods into separate classes mitigates the total responsibility of the Transaction class in line with SRP design guidelines.

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 Buy and 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 Buy and 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 Buy.makeRecord() and 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 Buy and 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 Buy and Sell without modifying those classes! An example of this approach is illustrated in the Open/Closed Principle UML diagram below:

open closed principle uml diagram example overcoded
Inheriting from the RecordableTransaction class allows extension of the makeRecord method without requiring modification among any child classes such as Buy or Sell

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 sell() and 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.buy() and RecordableTransaction.sell(). Consider the Liskov Substitution Principle UML Diagram example below:

liskov substitution principle uml diagram overcoded
This LSP UML Diagram illustrates the subclass RecordableTransaction which can be substituted for instances of Transaction

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 Buy, and RecordableBuyclasses to manage Actions.

The 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 Buy or Sell action—kind of like a receipt.

Implementing Buy and RecordableBuy from a common interface violates the ISP given that it would have to dictate Execute and MakeRecord methods for both. The Buy class does not need to make a record of its transactions.

Implementing from an interface common to Buy and RecordableBuy couple them to that functionality. A more ISP conforming approach is illustrated in the following Interface Segregation Principle UML example diagram:

Interface Segregation Principle UML example diagram overcoded
The makeRecord functionality is placed in a separate Interface to avoid forcing Buy from implementing it without need.

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:

dependency inversion principle uml example diagram overcoded
DIP implementation such that lower-level dependencies are abstracted out via interfaces

History

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).

Final Thoughts

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.

References

  1. Martin, Robert C. Agile Software Development, Principles, Patterns, and Practices. 1st ed., Pearson, 2002.
  2. Fowler, Martin, et al. Refactoring: Improving the Design of Existing Code. 1st ed., Addison-Wesley Professional, 1999.
  3. DeMarco, Tom. Structured Analysis and System Specification. 1st ed., Prentice-Hall, 1979.
  4. Meyer, Bertrand. Object-Oriented Software Construction. Prentice-Hall, 1994.
  5. 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.
Zαck West
Full-Stack Software Engineer with 10+ years of experience. Expertise in developing distributed systems, implementing object-oriented models with a focus on semantic clarity, driving development with TDD, enhancing interfaces through thoughtful visual design, and developing deep learning agents.