The term S.O.L.I.D. stands for five design principles intended to make software designs more understandable, flexible and maintainable. These are a subset of many principles promoted by Robert C. Martin and when combined together, they make it easy for a programmer to develop software that is easy to maintain, extend and refactor, while avoiding code smells. The SOLID principles also form a core philosophy for methodologies such as Agile development or Adaptive Software Development.
Understanding and applying these principles will allow you to write better quality code and therefore be a better developer.
In this article I refrain from using code examples as understanding the principles and their importance is the focus. Also, since these are language independent, I believe the SOLID principles are better explained with simple examples and metaphors than with lengthy code snippets. Next to my own experience, I read quite a few articles and books on OO-principles, but want to say special thanks to /u/CleverNameAndNumbers (Reddit) for the simple examples.
I also highly recommend reading Robert C. Martin's books The Clean Coder and Clean Code.
Mnemonic acronym
In short, the five S.O.L.I.D. principles are the following.
- Single responsibility principle - A class should have one, and only one, reason to change.
- Open/closed principle - You should be able to extend a class's behavior, without modifying it.
- Liskov substitution principle - Derived classes must be substitutable for their base classes.
- Interface segregation principle - Make fine grained interfaces that are client specific.
- Dependency inversion principle - Depend on abstractions, not on concretions.
These will be described in detail in the next chapters.
[S]ingle responsibility principle
The Single Responsibility Principle (SRP) states that a class should have only one reason to change.
Every module or class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class.
It's like saying that in an office, let the secretary be the secretary, the security guard be the security guard, the CEO be the CEO and the janitor be the janitor. It's a bad idea if you try to combine all those duties and drop it on just one person.
SRP - Coupling and cohesion
The Single responsibility principle is closely related to the concepts of coupling and cohesion.
Cohesion refers to what a class or module can do. When a class has low cohesion, it means that it does a great variety of actions and in that, the class is unfocused on what it should do. High cohesion means that the class is focused on what it should be doing as it contains only methods relating to the intention of the class.
Coupling refers to how related or dependent two classes or modules are towards each other. For low coupled classes, a major change in one class has a low impact on the other. High coupling in a system makes it difficult to maintain since a change in one class will have impact on other classes as well. This could result in one change flowing through a system as an oil slick, sometimes even requiring a full overhaul to fully implement.
Good software design consists of classes or modules with high cohesion and low coupling.
Uncle Bob on SRP
Robert C. Martin (Uncle Bob) states that we define each responsibility of a class as a reason for change. If there is more than one reason to change a class, it probably has more than one responsibility.
Gather together the things that change for the same reasons. Separate those things that change for different reasons.
In the context of cohesion and coupling, this means that in order to achieve this, we want to increase the cohesion between things that change for the same reasons, and we want to decrease the coupling between those things that change for different reasons.
In software, some examples of responsibilities that may need to be separated are the following:
- Notification
- Error handling
- Logging
- Formatting
- Parsing
- Persistence
- Validation
- etc.
[O]pen/closed principle
The Open-Closed Principle (OCP) states that software entities (classes, modules, methods, etc.) should be open for extension, but closed for modification.
In practice, this means that when you write a class, a function or library you should do it in such a way that anyone else can easily build on to it, but not change its core elements.
A very simple example of this concept would be an old photo camera that does not allow changing of lenses. It is not open for extension and thus violates the OCP. Most modern SLR cameras have interchangeable lenses and by that extending the functionality to achieve different photographic results with different lenses. Moreover, changing a lens does not modify the camera's basic functionality to take photo's.
The Open-Closed principle can be achieved in many other ways like through inheritance, or via behavioral design patterns like the Strategy design pattern.
[L]iskov substitution principle
The Liskov Substitution Principle (LSP), developed by Barbara Liskov, states that subtypes must be substitutable for their base types.
In other words, suppose object S is a subtype of object T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of T.
Consider a Rectangle class with a width and height as properties of which you want to create an inherited Square class. It would be bad to create a "SideLength" property for the Square class, since you would no longer be able to substitute the Square class by its base Rectangle class without modifying the functionality of the width and height properties of Rectangle.
A rectangle may have different widths and heights, thus having two different side lengths, opposed to a square that has one single side length by definition. Therefore, substituting Square by Rectangle will modify the base functionality of having a width and height that may be different from each other.
When this Liskov substitution principle is violated, it tends to result in a lot of extra conditional logic scattered throughout the application. This duplicate, scattered code becomes a breeding ground for bugs as the application grows. A common code smell that is often an indication of a LSP violation is the presence of type checking code within a code block that should be polymorphic.
[I]nterface segregation principle
The Interface Segregation Principle (ISP) states that clients should not be forced to depend on methods that they do not use.
In short: only make the user implement whichever methods they intent to use. This problem is commonly found in interfaces. An interface is a collection of functions that have a name and defined input and output parameters, but no implementation. A class implementing this interface needs to implement all functions in the interface.
If you only intend to use a handful of relevant functions, but there are 20 others in the interface, there would be 20 functions in your class doing nothing. Instead, it is better to split up the interface in its most relevant groups.
Suppose you're a car mechanic who only offers two services: a complete overhaul of the car from top to bottom, or nothing at all. Nobody would come to your shop. Instead, you should offer an oil change package, a tire replacement package, brake checkup package, etc.
[D]ependency inversion principle
The Dependency Inversion Principle (DIP) states that:
- High level modules should not depend on low level modules; both should depend on abstractions.
- Abstractions should not depend on details, the details should depend on the abstractions.
A proper high-level module should be flexible on how its lower level modules operate and should not have to assume a certain way of operating based on the way the lower level modules operate. Likewise, a low-level module should not be concerned to which top level module it's reporting.
Both high- and low-level modules should not have each other's specific details, but rather just share a specification that will suit any application.
Usually this is achieved by using an interface between the high- and low-levels. A high level module can call the same function on all low level modules, knowing that the input and output will always be of the expected type. The low level reports to the interface using the same logic.
Suppose you're working at a company as a manager. The company has many employees and there are many things that need to be done. The employees don't only work for you, but have other managers to report to. Instead of teaching them how to write their report specifically for you, you should teach them the basic principles of that report and do the formalization yourself.
From an employees perspective, it would be overwhelming to memorize the reporting preferences of every manager. It would be much easier if they can just write a generic report with a certain set of data that's suitable for every manager.
The generic report process is the interface between manager and employee. All that you require as a manager is the data, since you know what the output should look like.
By doing this, you as a manager aren't reliant on having your employees giving their reports in a very specific fashion (you'd have to proof read anyway). You can simply give your employees the data they need. If those employees have to give that report to anyone else, they won't be confused by the specific content you put on yours.
As an employee, this allows you to work for multiple managers using only a simple set of rules, rather than memorizing a huge stack of little details for each manager.
Closing thoughts
In general, software should be written as simply as possible in order to produce the desired result. Once updating the software becomes painful, its design should be adjusted to remove the pain (Pain driven development). These five SOLID principles, in addition to the more general Don’t Repeat Yourself (DRY) principle, can be used as a guide while refactoring the software into a better design.