One of the most common statements that many developers make regarding object-oriented programming is that a primary advantage of OOP is that it models the real world. I admit that I use these words a lot when I discuss classical object-oriented concepts. According to Robert Martin (in at least one lecture that I viewed on YouTube), the idea that OO is closer to the way we think is simply marketing. Instead, he states that OO is about managing dependencies by inverting key dependencies to prevent rigid code, fragile code, and non-reusable code.
For example, in classical object-oriented programming courses, the practice often models the code directly to real-life situations. For example, if a dog is-a mammal, then this relationship is an obvious choice for inheritance. The strict has-a and is-a litmus test has been part of the OO mindset for years.
However, as we have seen throughout this book, trying to force an inheritance relationship can cause design problems (remember the barkless dog?). Is trying to separate barkless dogs from barking dogs, or flying birds from flightless birds, a smart inheritance design choice? Was this all put in place by object-oriented marketers? OK; forget the hype. As we saw in the previous chapter, perhaps focusing on a strict has-a and is-a decision is not necessarily the best approach. Perhaps we should focus more on decoupling the classes.
In the lecture I mentioned previously, Robert Martin, often referred to as Uncle Bob, defines these three terms to describe non-reusable code:
Rigidity—When a change to one part of a program can break another part
Fragility—When things break in unrelated places
Immobility—When code cannot be reused outside its original context
SOLID was introduced to address these problems and strive to attain these goals. It defines five design principles that Robert Martin introduced to “make software designs more understandable, flexible, and maintainable.” According to Robert Martin, though they apply to any object-oriented design, the SOLID principles can also form a core philosophy for methodologies such as agile development or adaptive software development. The SOLID acronym was introduced by
Michael Feathers.
The five SOLID principles are
SRP—Single Responsibility Principle
OCP—Open/Close Principle
LSP—Liskov Substitution Principle
IPS—Interface Segregation Principle
DIP—Dependency Inversion Principle
This chapter focuses on covering these five principles and relates them to the classical object-oriented principles that have been in place for decades. My goal in covering SOLID is to explain the concepts in very simple examples. There is a lot of content online, including several very good YouTube videos. Many of these videos target developers, not necessarily students new to programming.
As I have attempted to do with all the examples in this book, my intent is not to get overly complicated but to distill the examples to the lowest common denominator for educational purposes.
In Chapter 11, “Avoiding Dependencies and Highly Coupled Classes,” we discussed some of the fundamental concepts leading up to our discussion of the five SOLID principles. In this chapter, we dive right in and cover each of the SOLID principles in more detail. All SOLID definitions are from the Uncle Bob site: http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod.
The Single Responsibility Principle states that a class should have only a single reason to change. Each class and module in a program should focus on a single task. Thus, don’t put methods that change for different reasons in the same class. If the description of the class includes the word “and,” you might be breaking the SRP. In other words, 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 in the class.
Creating a shape hierarchy is one of the classic illustrations of inheritance. It is used often as a teaching example, and I use it a lot throughout this chapter (as well as the book). In this example, a Circle
class inherits from an abstract Shape
class. The Shape
class provides an abstract method called calcArea()
as the contract for the subclass. Any class that inherits from Shape
must provide its own implementation of calcArea()
:
abstract class Shape{ protected String name; protected double area; public abstract double calcArea(); }
In this example, we have a Circle
class that inherits from Shape
and, as required, provides its implementation of calcArea()
:
class Circle extends Shape{ private double radius; public Circle(double r) { radius = r; } public double calcArea() { area = 3.14*(radius*radius); return (area); }; }
In this example, we are only going to include a Circle class to focus on the Single Responsibility Principle and keep the example as simple as possible.
A third class called CalculateAreas
sums the areas of different shapes contained in a Shape
array. The Shape
array is of unlimited size and can contain different shapes, such as squares and triangles.
class CalculateAreas { Shape[] shapes; double sumTotal=0; public CalculateAreas(Shape[] sh){ this.shapes = sh; } public double sumAreas() { sumTotal=0; for (inti=0; i<shapes.length; i++) { sumTotal = sumTotal + shapes[i].calcArea(); } return sumTotal; } public void output() { System.out.println("Total of all areas = " + sumTotal); } }
Note that the CalculateAreas
class also handles the output for the application, which is problematic. The area calculation behavior and the output behavior are coupled—contained in the same class.
We can verify that this code works with the following test application called TestShape
:
public class TestShape { public static void main(String args[]) { System.out.println("Hello World!"); Circle circle = new Circle(1); Shape[] shapeArray = new Shape[1]; shapeArray[0] = circle; CalculateAreas ca = new CalculateAreas(shapeArray); ca.sumAreas(); ca.output(); } }
Now with the test application in place, we can focus on the issue of the Single Responsibility Principle. Again, the issue is with the CalculateAreas
class and that this class contains behaviors for summing the various areas as well as the output.
The fundamental point (and problem) here is this: If you want to change the functionality of the output()
method, it requires a change to the CalculateAreas
class regardless of whether the method for summing the areas changes. For example, if at some point we want to present the output to the console in HTML rather than in simple text, we must recompile and redeploy the code that sums the area because the responsibilities are coupled.
According to the Single Responsibility Principle, the goal is that a change to one method would not affect the other method, thus preventing unnecessary recompilations. “A class should have one, and only one, reason to change—a single responsibility to change.”
To address this, we can put the two methods in separate classes, one for the original console output and one for the newly included HTML output:
class CalculateAreas { Shape[] shapes; double sumTotal=0; public CalculateAreas(Shape[] sh){ this.shapes = sh; } public double sumAreas() { sumTotal=0; for (inti=0; i<shapes.length; i++) { sumTotal = sumTotal + shapes[i].calcArea(); } return sumTotal; } } class OutputAreas { double areas=0; public OutputAreas(double a){ this.areas = a; } public void console() { System.out.println("Total of all areas = " + areas); } public void HTML() { System.out.println("<HTML>"); System.out.println("Total of all areas = " + areas); System.out.println("</HTML>"); } }
Now, using the newly written class, we can add functionality for HTML output without impacting the code for the area summing:
public class TestShape { public static void main(String args[]) { System.out.println("Hello World!"); Circle circle = new Circle(1); Shape[] shapeArray = new Shape[1]; shapeArray[0] = circle; CalculateAreas ca = new CalculateAreas(shapeArray); CalculateAreas sum = new CalculateAreas(shapeArray); OutputAreasoAreas = new OutputAreas(sum.sumAreas()); oAreas.console(); // output to console oAreas.HTML(); // output to HTML } }
The main point here is that you can now send the output to various destinations depending on requirements. If you want to add another output possibility, such as JSON, you can add it to the OutputAreas
class without having to change the CalculateAreas
class. As a result, you can redistribute the CalculateAreas
class independently without having to do anything to the other classes.
The Open/Close Principle states that you should be able to extend a class’s behavior, without modifying it.
Let’s revisit the shape example yet again. In the following code, we have a class called ShapeCalculator
that accepts a Rectangle
object, calculates the area of that object, and then returns that value. It is a simple application but it works only for rectangles.
class Rectangle{ protected double length; protected double width; public Rectangle(double l, double w) { length = l; width = w; }; } class CalculateAreas { private double area; public double calcArea(Rectangle r) { area = r.length * r.width; return area; } } public class OpenClosed { public static void main(String args[]) { System.out.println("Hello World"); Rectangle r = new Rectangle(1,2); CalculateAreas ca = new CalculateAreas (); System.out.println("Area = "+ ca.calcArea(r)); } }
The fact that this application works only for rectangles brings us to a constraint that illustrates the Open/Closed Principle: If we want to add a Circle
to the CalculateArea
class (change what it does), we must change the module itself. Obviously, this is at odds with the Open/Closed Principle, which stipulates that we should not have to change the module to change what it does.
To comply with the Open/Closed Principle, we can revisit our tried and true shape example, where an abstract class called Shape
is created and then all shapes must inherit from the Shape
class, which has an abstract method called getArea()
.
At this point, we can add as many different classes as we want without having to change the Shape
class itself (for example, a Circle
). We can now say that the Shape
class is closed.
The following code implements this solution for a rectangle and a circle, and allows for the creation of unlimited shapes:
abstract class Shape { public abstract double getArea(); } class Rectangle extends Shape { protected double length; protected double width; public Rectangle(double l, double w) { length = l; width = w; }; public double getArea() { return length*width; } } class Circle extends Shape { protected double radius; public Circle(double r) { radius = r; }; public double getArea() { return radius*radius*3.14; } } class CalculateAreas { private double area; public double calcArea(Shape s) { area = s.getArea(); return area; } } public class OpenClosed { public static void main(String args[]) { System.out.println("Hello World"); CalculateAreas ca = new CalculateAreas(); Rectangle r = new Rectangle(1,2); System.out.println("Area = " + ca.calcArea(r)); Circle c = new Circle(3); System.out.println("Area = " + ca.calcArea(c)); } }
Note that in this implementation, the CalculateAreas()
method does not have to change when you add a new Shape
.
You can scale your code without having to worry about legacy code. At its core, the Open/Closed Principle states that you should extend your code via subclasses and the original class does not need to be changed. However, the word extension is problematic in several discussions relating to SOLID. As we will cover in detail, if we are to favor composition over inheritance, how does this affect the Open/Closed Principle?
When following one of the SOLID principles, code may also comply with one of the other SOLID principles. For example, when designing to follow the Open/Closed Principle, the code may also comply with the Single Responsibility Principle.
The Liskov Substitution Principle states that the design must provide the ability to replace any instance of a parent class with an instance of one of its child classes. If a parent class can do something, a child class must also be able to do it.
Let’s examine some code that might look reasonable but violates the Liskov Substitution Principle. In the following code, we have the typical abstract class called Shape
. Rectangle
then inherits from Shape
and overrides its abstract method calcArea()
. Square
, in turn, inherits from Rectangle
.
abstract class Shape{ protected double area; public abstract double calcArea(); } class Rectangle extends Shape{ private double length; private double width; public Rectangle(double l, double w){ length = l; width = w; } public double calcArea() { area = length*width; return (area); }; } class Square extends Rectangle{ public Square(double s){ super(s, s); } } public class LiskovSubstitution { public static void main(String args[]) { System.out.println("Hello World"); Rectangle r = new Rectangle(1,2); System.out.println("Area = " + r.calcArea()); Square s = new Square(2); System.out.println("Area = " + s.calcArea()); } }
So far so good: a rectangle is-a shape so everything looks fine. Because a square is-a rectangle we are still fine—or are we?
Now we enter into a somewhat philosophical discussion: Is a square really a rectangle? Many people would say yes. However, while the square may well be a specialized type of a rectangle, it does have different properties than a rectangle. A rectangle is a parallelogram (opposite sides are congruent), as is a square. Yet, a square is also a rhombus (all sides are congruent), whereas a rectangle is not. Therefore, there are some differences.
The geometry is not really the issue when it comes to OO design. The issue is how we build rectangles and squares. Here is the constructor for the Rectangle
class:
public Rectangle(double l, double w){ length = l; width = w; }
The constructor obviously requires two parameters. However, the Square
constructor requires just one, even though its parent class, Rectangle
, is expecting two.
class Square extends Rectangle{ public Square(double s){ super(s, s); }
In actuality, the functionality to compute area is subtly different for the two classes. In fact, the Square
is kind of faking the Rectangle
out by passing it the same parameter twice. This may seem like an acceptable workaround, but it really is something that may confuse someone maintaining the code and could very well cause unintended maintenance headaches down the road. This is an inconsistency at minimum and, perhaps, a questionable design decision. When you see a constructor calling another constructor, it might be a good idea to pause and reconsider the design—it might not be a proper child class.
How do you address this specific dilemma? Simply put, a square is not a substitute for a rectangle and should not be a child class. Thus, they should be separate classes.
abstract class Shape { protected double area; public abstract double calcArea(); } class Rectangle extends Shape { private double length; private double width; public Rectangle(double l, double w) { length = l; width = w; } public double calcArea() { area = length*width; return (area); }; } class Square extends Shape { private double side; public Square(double s){ side = s; } public double calcArea() { area = side*side; return (area); }; } public class LiskovSubstitution { public static void main(String args[]) { System.out.println("Hello World"); Rectangle r = new Rectangle(1,2); System.out.println("Area = " + r.calcArea()); Square s = new Square(2); System.out.println("Area = " + s.calcArea()); } }
The Interface Segregation Principle states that it is better to have many small interfaces than a few larger ones.
In this example, we are creating a single interface that includes multiple behaviors for a Mammal
, eat()
and makeNoise()
:
interface IMammal { public void eat(); public void makeNoise(); } class Dog implements IMammal { public void eat() { System.out.println("Dog is eating"); } public void makeNoise() { System.out.println("Dog is making noise"); } } public class MyClass { public static void main(String args[]) { System.out.println("Hello World"); Dog fido = new Dog(); fido.eat(); fido.makeNoise(); } }
Rather than creating a single interface for Mammal
, we can create separate interfaces for all the behaviors:
interface IEat { public void eat(); } interface IMakeNoise { public void makeNoise(); } class Dog implements IEat, IMakeNoise { public void eat() { System.out.println("Dog is eating"); } public void makeNoise() { System.out.println("Dog is making noise"); } } public class MyClass { public static void main(String args[]) { System.out.println("Hello World"); Dog fido = new Dog(); fido.eat(); fido.makeNoise(); } }
In reality, we are decoupling the behaviors from the Mammal
class. Thus, rather than creating a single Mammal
entity via inheritance (actually interfaces) we are moving to a composition-based design, similar to the strategy taken in the previous chapter.
In short, by using this approach, we can build Mammal
s with composition rather than being forced to utilize behaviors contained in a single Mammal
class. For example, suppose someone discovers a Mammal
that doesn’t eat but instead absorbs nutrients through its skin. If we were inheriting from a single Mammal
class that contains the eat()
behavior, the new mammal would not need this behavior. However, if we separate all the behaviors into separate, single interfaces, we can build each mammal in exactly the way it presents itself.
The Dependency Inversion Principle states that code should depend on abstractions. It often seems like the terms dependency inversion and dependency injection are used interchangeably; however, here are some key terms to understand as we discuss this principle:
Dependency inversion—The principle of inverting the dependencies
Dependency injection—The act of inverting the dependencies
Constructor injection—Performing dependency injection via the constructor
Parameter injection—Performing dependency injection via the parameter of a method, like a setter
The goal of dependency inversion is to couple to something abstract rather than concrete.
Although at some point you obviously have to create something concrete, we strive to create a concrete object (by using the new
keyword) as far up the chain as possible, such as in the main()
method. Perhaps a better way of thinking of this is to revisit the discussion presented in Chapter 8,
“Frameworks and Reuse: Designing with Interfaces and Abstract Classes,” where we discuss loading classes at runtime, and in Chapter 9, “Building Objects and Object-Oriented Design,” where we talk about decoupling and creating small classes with limited responsibilities.
In the same vein, one of the goals of the Dependency Inversion Principle is to choose objects at runtime, not at compile time. (You can change the behavior of your program at runtime.) You can even write new classes without having to recompile old ones (in fact, you can write new classes and inject them).
Much of the foundation for this discussion was put forth in Chapter 11, “Avoiding Dependencies and Highly Coupled Classes.” Let’s build on that as we consider the Dependency Inversion Principle.
For the first step in this example, we revisit yet again one of the classical object-oriented design examples used throughout this book, that of a Mammal
class, along with a Dog
and a Cat
class that inherit from Mammal
. The Mammal
class is abstract and contains a single method called makeNoise()
.
abstract class Mammal { public abstract String makeNoise(); }
The subclasses, such as Cat
, use inheritance to take advantage of Mammal
’s behavior, makeNoise()
:
class Cat extends Mammal { public String makeNoise() { return "Meow"; } }
The main application then instantiates a Cat
object and invokes the makeNoise()
method:
Mammal cat = new Cat();; System.out.println("Cat says " + cat.makeNoise());
The complete application for the first step is presented in the following code:
public class TestMammal { public static void main(String args[]) { System.out.println("Hello World "); Mammal cat = new Cat();; Mammal dog = new Dog(); System.out.println("Cat says " + cat.makeNoise()); System.out.println("Dog says " + dog.makeNoise()); } } abstract class Mammal { public abstract String makeNoise(); } class Cat extends Mammal { public String makeNoise() { return "Meow"; } } class Dog extends Mammal { public String makeNoise() { return "Bark"; } }
The preceding code has a potentially serious flaw: It couples the mammals and the behavior (makingNoise
). There may be a significant advantage to separating the mammal behaviors from the mammals themselves. To accomplish this, we create a class called MakingNoise
that can be used by all mammals as well as non-mammals.
In this model, a Cat
, Dog
, or Bird
can then extend the MakeNoise
class and create their own noise-making behavior specific to their needs, such as the following code fragment for a Cat
:
abstract class MakingNoise { public abstract String makeNoise(); } class CatNoise extends MakingNoise { public String makeNoise() { return "Meow"; } }
With the MakingNoise
behavior separated from the Cat
class, we can use the CatNoise
class in place of the hard coded behavior in the Cat
class itself, as the following code fragment illustrates:
abstract class Mammal { public abstract String makeNoise(); } class Cat extends Mammal { CatNoise behavior = new CatNoise(); public String makeNoise() { return behavior.makeNoise(); } }
The following is the complete application for the second step:
public class TestMammal { public static void main(String args[]) { System.out.println("Hello World "); Mammal cat = new Cat();; Mammal dog = new Dog(); System.out.println("Cat says " + cat.makeNoise()); System.out.println("Dog says " + dog.makeNoise()); } } abstract class MakingNoise { public abstract String makeNoise(); } class CatNoise extends MakingNoise { public String makeNoise() { return "Meow"; } } class DogNoise extends MakingNoise { public String makeNoise() { return "Bark"; } } abstract class Mammal { public abstract String makeNoise(); } class Cat extends Mammal { CatNoise behavior = new CatNoise(); public String makeNoise() { return behavior.makeNoise(); } } class Dog extends Mammal { DogNoise behavior = new DogNoise(); public String makeNoise() { return behavior.makeNoise(); } }
The problem is that although we have decoupled a major part of the code, we still haven’t reached our goal of dependency inversion because the Cat
is still instantiating the Cat
noise-making behavior.
CatNoise behavior = new CatNoise();
The Cat
is coupled to the low-level module CatNoise
. In other words, the Cat
should not be coupled to CatNoise
but to the abstraction for making noise. In fact, the Cat
class should not instantiate its noise-making behavior but instead receive the behavior via injection.
In this final step, we totally abandon the inheritance aspects of our design and examine how to utilize dependency injection via composition. You do not need inheritance hierarchies, which is one of the major reasons why the concept of composition over inheritance is gaining momentum. You compose a subtype rather than creating a subtype from a hierarchical model.
To illustrate, in the original implementation, the Cat
and the Dog
basically contain the same exact code; they simply return a different noise. As a result, a significant percentage of the code is redundant. Thus, if you had many different mammals, there would be a lot of noise-making code. Perhaps a better design is to take the code to make noise out of the mammal.
The major leap here would be to abandon the specific mammals (Cat
and Dog
) and simply use the Mammal
class as shown here:
class Mammal { MakingNoise speaker; public Mammal(MakingNoisesb) { this.speaker = sb; } public String makeNoise() { return this.speaker.makeNoise(); } }
Now we can instantiate a Cat
noise-making behavior and provide it to the Animal
class, to make a mammal that behaves like a Cat
. In fact, you can always assemble a Cat
by injecting behaviors rather than using the traditional techniques of class building.
Mammal cat = new Mammal(new CatNoise());
The following is the complete application for the final step:
public class TestMammal { public static void main(String args[]) { System.out.println("Hello World "); Mammal cat = new Mammal(new CatNoise()); Mammal dog = new Mammal(new DogNoise()); System.out.println("Cat says " + cat.makeNoise()); System.out.println("Dog says " + dog.makeNoise()); } } class Mammal { MakingNoise speaker; public Mammal(MakingNoisesb) { this.speaker = sb; } public String makeNoise() { return this.speaker.makeNoise(); } } interface MakingNoise { public String makeNoise(); } class CatNoise implements MakingNoise { public String makeNoise() { return "Meow"; } } class DogNoise implements MakingNoise { public String makeNoise() { return "Bark"; } }
When discussing dependency injection, when to actually instantiate an object is now a key consideration. Even though the goal is to compose objects via injection, you obviously must instantiate objects at some point. As a result, the design decisions revolve around when to do this instantiation.
As stated earlier in this chapter, the goal of dependency inversion is to couple to something abstract rather than concrete, even though you obviously must create something concrete at some point. Thus, one simple goal is to create a concrete object (by using new
) as far up the chain as possible, such as in the main()
method. Always evaluate things when you see a new
keyword.
This concludes the discussion of SOLID. The SOLID principles are one of the most influential sets of object-oriented guidelines used today. What is interesting about studying these principles is how they relate to the fundamental object-oriented encapsulation, inheritance, polymorphism, and composition, specifically in the debate of composition over inheritance.
For me, the most interesting point to take away from the SOLID discussion is that nothing is cut and dried. It is obvious from the discussion on composition over inheritance that even the age-old fundamental OO concepts are open for reinterpretation. As we have seen, a bit of time, along with the corresponding evolution in various thought processes, is good for innovation.
Martin, Robert, et al. Agile Software Development, Principles, Patterns, and Practices. 2002. Boston: Pearson Education, Inc.
Martin, Robert, et al. Clean Code. 2009. Boston: Pearson Education, Inc.