The SOLID principles of Object-Oriented Design

Jonas
Captures
Published in
4 min readJun 9, 2019

The SOLID principles were first conceptualized by Robert C. Martin in his 2000 paper, Design Principles and Design Patterns. These 5 principles have revolutionized the world of object-oriented programming, changing the way that we write software.

The following 5 concepts make up S.O.L.I.D. principles:

  1. Single Responsibility
  2. Open/Closed
  3. Liskov Substitution
  4. Interface Segregation
  5. Dependency Inversion

In the following sections, I’ll explain what each of these principles along with couple of code snippets.

Single Responsibility

A single responsibility principle (SRP) states that

  • A class should have one, and only one responsibility.
  • A class should have only one reason to change.

A few of its benefits to help us building better software:

  1. Testing — A class with one responsibility will have far fewer test cases.
  2. Lower coupling — Less functionality in a single class will have fewer dependencies.
  3. Organization — Smaller, well-organized classes are easier to sustain.

Take, for example, a class to represent an Employee’s personal information:

To be simple, we don’t implement persistent method yet, so we would store many employees in memory for temporary.

Here comes requirements from HR and IT departments, HR requests a list of first name, last name, and wage, however IT requests a list of all personal information excluding sensitive wage.

Developers might add two print methods for different purpose:

The above code does fulfill the requirements, however, it also violates the SRP. The class will be probably modified by two reasons, one is from HR and another is from IT. We should implement two separate classes that is concerned only with one department requirement:

We’ve developed a class that relieves the Employee class of its printing duties.

Design Considerations

As you design with SRP in mind, you still have to tackle with two problems:

  1. It’s difficult to decide in the beginning, how fine and coarse should a class be?
  2. It would be increasing complexity as you design a class too fine grained.

Open/Closed

An open-closed principle (OCP) states that

  • A class should be open for extension, but closed for modification.

The principle also stop ourselves from modifying existing code and causing potential new defects. Of course, the only exception is when fixing defects in existing code.

We already defined Employee class and the entire ERP system centers around this class. However, after a few months, Sales team requests adding additional bonus in monthly wage, and HR could pay upon the wage.

At this point, it might be tempting to open up the Employee class and add a bonus property which default value 0 along with a setter function for Sales team, the wage property also has to be modified accordingly — but who knows what errors that might throw up in the ERP system.

Instead, let’s adhere to the OCP and simply extend existing Employee class:

By extending the Employee class we can be sure that our existing application won’t be affected.

Liskov Substitution

A Liskov substitution principle (LSP) states that

  • A class C is a subtype of class P, then we should be able to replace P with C without disrupting the behavior of the application.
  • Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it.

Or simply, a derived class should be substitutable for a parent class. Let’s go straight to the code to help wrap our heads around this concept:

Above, we defined a simple Bird interface that all birds should be able to walk and fly— the hourly speed of walk or fly distance. Let’s implement the interface for both dove and eagle:

What about a penguin?

We are inherently changing the behavior of the application. This is an apparent violation of LSP, one possible solution would be to rework class that take into account flyable and dis-flyable Bird.

Interface Segregation

An interface segregation principle (ISP) states that

  • Larger interfaces should be split into smaller ones. By doing so, we can ensure that implementing classes only need to be concerned about the methods that are of interest to them.
  • A derived class should not be forced to implement an interface that it doesn’t use.

Taking previous example and take ISP into consideration, the Penguin class has no choice to implement the fly interface. Let’s fix this by splitting our interfaces into 2 separate ones:

Now, we’re able to implement only the interfaces that matter to penguin:

Dependency Inversion

A 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. Details should depend on abstractions.

Or simply, a class should depend on abstractions not on concretions. Let’s take Employee example again, the developers are told to implement a way to serialize Employee class in data store:

Apparently Employee class depends on MySQLStore class, hence this violates high-level module Employee class should not depend on low-level module MySQLStore class.

If we want to change the data store from MySQLStore class to MongoDBStore class, we have to change hard-coded constructor in Employee class. Employee class should depend on abstractions not on concretions. Please see how we fix it in the following code:

In the above code, we want to change the data store from MySQLStore class to MongoDBStore class, we no need to change method injection in Employee class. Because Employee class depends on abstractions, not on concretions anymore.

Excellent! We’ve decoupled the dependencies and are free to test Employee class with mocked DBStore class.

Conclusion

S.O.L.I.D. principles are for good object-oriented design. Most of the principles involve adding a layer of abstraction between classes that would otherwise be dependent on each other, thus creating a loose coupling relationship resulting in less rigid and fragile code, which means easier maintainability and extensibility.

I would recommend you keeping these principles in mind when writing new code. See what principles your code violates and how you can rework the code to adhere to the principles that were violated. You will end up with cleaner code that will make things easier as your code grows.

--

--