SOLID is an acronym that stands for five widely accepted principles of object-oriented programming and design. These principles are:
-
Single Responsibility Principle
-
Open/Closed Principle
-
Liskov Substitution Principle
-
Interface Segregation Principle
-
Dependency Inversion Principle
Robert “Uncle Bob” Martin is acknowledged as the first person to identify the SOLID principles, although the SOLID acronym was later introduced by Michael Feathers. |
A recurring theme throughout the SOLID principles is the avoidance of dependencies. It makes intuitive, practical, and theoretical sense to minimize class dependencies: the more things you depend on, the greater the chance something will go wrong.
1. Single Responsibility Principle
The Single Responsibility Principle (SRP) states that a class should have exactly one responsibility. It should have exactly one reason (or one class of reasons) that cause it to be changed. If you have ever tried to modify a tangled mass of code in which tangential concerns have been mixed up together, where a seemingly innocuous change has unexpected and disastrous consequences, then you might have been a victim of code that ignores this principle.
To illustrate the point, a violation of this principle might look like this:
var myReport = new CashflowReport();
formatReportHeader(myReport);
printReport(myReport);
void formatReportHeader(Report report) {
report.Header.Bold = true;
if (report is CashflowReport && report.Content.Profit < 10000) {
SendAlertToCEO("Profit warning!");
}
}
All the presented source code is written in C#. |
All well and good—until the CEO asks for the report header to be reset to the default format, without any bold bits. The maintenance programmer (maintenance programmers are usually harassed and short of time) looks at the first few lines of code, and because it appears to be well-structured she innocently deletes the line that calls formatReportHeader
. The code now looks like this:
var myReport = new CashflowReport();
printReport(myReport);
void formatReportHeader(Report report) {
report.Header.Bold = true;
if (report is CashflowReport && report.Content.Profit < 10000) {
SendAlertToCEO("Profit warning!");
}
}
Testing confirms that the report header is no longer emboldened, so the modified system is released to the customer. In this disastrous scenario, warnings about a low profit are no longer sent to the CEO and the business quickly becomes insolvent.
I’ve provided an over-simplified and artificial example that illustrates the point. In real projects it won’t be anywhere near as obvious that a small change in one area of the system might affect an unrelated but critical feature in an unrelated part of the system. It isn’t hard to see why some programmers develop superstitions about the systems they maintain. Don’t touch the Frobbit module. The last programmer who changed it was blamed for the extinction of pink-frilled lizards.
Another difficulty many programmers have with following the SRP lies in deciding what single responsibility means. This definition can be hard to pin down. You can take a broad view that a single responsibility refers to a group of related business processes (such as invoicing, for example) or you can take a stricter view that invoicing is far too broad and should be broken down in its constituent parts, with each part designated as a responsibility. How far you go depends on your stamina and tolerance. Cross-cutting concerns also muddy the water: Is it consistent with SRP to call logging routines from an invoicing class? (No it isn’t, but everyone does it.)
Like many principles, the theory is easier to grasp than the practice. In practice, you must rely on experience and common sense. You really shouldn’t remove all your logging from a class, that would be counterproductive, but you really do want to avoid mixing up code for formatting a report with code for enforcing business logic.
2. Open/Closed Principle
The Open/Closed Principle (OCP) states that a class (or function) should be open for extension but closed for modification. This principle attempts to counter the tendency for object-oriented code to become fragile or easily broken, when base classes are modified in ways that break the behavior of inheriting (child) classes.
It hasn’t escaped my attention that one of the key benefits touted for object-oriented programming is the ease with which you can change an entire inheritance chain through modification of the base class. If all your animals (textbook examples of inheritance for some reason seem to favor animals) are to be endowed with a new “panic” behavior, for instance, then you simply add a panic
method to the base Animal
class. All animals can now panic
, even the ones that carry towels and that should therefore be immune to this kind of emotional distress.
For a better understanding of the towel reference, see this reference to The Hitchhiker’s Guide to the Galaxy. |
In a nutshell, adhering to the OCP means that when adding new behavior you should leave base classes alone and instead create new, inheriting classes, adding behavior to these instead of the base class, and thereby avoiding the problem of unintended consequences for classes that inherit from the same base class.
3. Liskov Substitution Principle
One of the nice things about inheriting from a class is that you can pass this new class to an existing function that has been written to work with the base class, and that function will perform its work just as if you had passed an instance of the base class.
The Liskov Substitution Principle (LSP) is intended to keep that happy working relationship between classes and functions alive and well.
The LSP is similar to the Open/Closed Principle. Both the OCP and the LCP imply that you should avoid modifying the behavior of a base class, but the LSP forbids modification of that behavior through the mechanism of inheritance. LSP states that if type S inherits from type T then both T and S should be interchangeable in functions that expect T.
In other words, if you follow the LSP, you should be free to substitute a child class in a function that expects to deal with the base class. If you can’t, you’re looking at a violation of the LSP.
But wait! Doesn’t this principle undermine one of the key benefits of object-oriented programming? Isn’t it a feature of object-oriented programming that you can modify the behavior of a class through inheritance?
Indeed it is. However, experience tells us that changing the behavior of a class in many cases leads to problems elsewhere in a code base. It is another example of how you should strive to minimize dependencies of all kinds in the interest of writing robust and maintainable software.
In many cases, instead of inheritance, programmers should create classes that are composed of base classes. In other words, if the programmer wants to use the methods or properties of a class, she can create an instance of that class rather than inheriting from it. By avoiding inheritance, the programmer also avoids violation of the LSP. Functions that expect to deal with a certain base class are guaranteed to remain unaffected by the new class, and the code base of an application is therefore more robust and less likely to break.
In C#, you can prevent the inheritance of a class by using the sealed
keyword. In Java, you can use the final
keyword.
4. Interface Segregation Principle
The Interface Segregation Principle (ISP) is very simple. It says to avoid writing monstrous interfaces that burden classes with responsibilities they don’t need or want. You should instead create a collection of smaller, discrete interfaces, partitioning interface members according to what they concern. Classes can then pick and choose what they implement rather than having to swallow all or nothing.
Here is one of these monstrous interfaces:
public interface IOneInterfaceToRuleThemAll {
void DoThis();
void DoThat();
void GoHere();
void GoThere();
bool MyFavoriteTogglyThing {get; set;}
string FirstName {get; set;}
string LastName {get; set;}
}
Here is a nicer alternative; a collection of interfaces that segregate different areas of concern:
public interface IActionable {
void DoThis();
void DoThat();
}
public interface IMovable {
void GoHere();
void GoThere();
}
public interface IToggly {
bool MyFavoriteTogglyThing {get; set;}
}
public interface INamed {
string FirstName {get; set;};
string LastName {get; set;}
}
Nothing prevents the programmer from creating a class that implements all of these interfaces, but with smaller interfaces the programmer is free to implement just those parts she needs, ignoring the irrelevant parts. The programmer can now create one class that moves and toggles, and another class that performs actions and has a name.
5. Dependency Inversion Principle
The Dependency Inversion Principle (DIP) says to “depend upon abstractions, not upon concretions.” Somewhat surprisingly it turns out that “concretions” is in fact a real word (I checked). What this principle means is that instead of writing code that refers to actual classes, you should instead write code that refers to interfaces or perhaps abstract classes.
This is consistent with writing code that has minimal dependencies. If class A instantiates concrete class B, then these two classes are now bound together. If instead of a concrete class you rely on an interface IB, then the concrete class that implements IB can (in theory) be switched out for a different class through dependency injection.
In the following code, class A has a dependency on class B:
class B {
// ...
}
class A {
public void DoSomething() {
B b = new B(); // A now depends on B
}
}
Here is the same code, rewritten to depend on an interface IB rather than the concrete class B:
interface IB {
// ...
}
class B : IB {
// ...
}
class AlternativeB : IB {
// ...
}
class A {
public void DoSomething(IB b) {
// b can be either B or AlternativeB
}
}
Incidentally, but very conveniently, now that you are free to substitute any class you choose, it becomes trivial to substitute fake or mock classes when writing unit tests, avoiding the overhead of classes that aren’t needed for the sake of the test.
A whole class of tools exists for introducing dependency injection to legacy code. If programmers writing this legacy code had created dependencies on interfaces rather than on concrete classes, we wouldn’t need these tools.