top of page
Search
Writer's pictureTuyen Nguyen

S.O.L.I.D - The First 5 Principles of Object-Oriented Programing

Updated: Jul 29

S.O.L.I.D is an acronym for the first five object-oriented design (OOD) principles by Robert C. Martin, as known as Uncle Bob, in his paper Design Principles and Design Patterns.


These five principles, combined together, make software designs more understandable, easier to maintain and easier to extend.


S.O.L.I.D stands for:

  • S - Single-responsibility principle

  • O - Open-closed principle

  • L - Liskov substitution principle

  • I - Interface segregation principle

  • D - Dependency Inversion Principle

Let's have a look at each individually to understand why S.O.L.I.D can help us with better software design.

 

Single-Responsibility Principle (SRS)


It states that:

"A class should have one and only one reason to change, meaning that a class should have only one job."


Let's do an example of how to write a piece of code that violates this principle.

Imagine that a company has workers be responsible for 1 of 3 jobs:

  1. Develop software - Developers

  2. Test software - Tester

  3. Sale software - Salesman

Each worker will have a corresponding position and do their work.

What if?

  • The company adds more positions, so we have to rewrite class Employee, add more methods?

  • Does a developer do saleSoftware() or a salesman do developSoftware()?

This will cause a massive impact when your project becomes bigger and bigger in the near future.

This is where we should apply SRP - a class should have only one job.

Solution:

  1. Create an abstract class Employee has an abstract method working().

  2. Three class Developer, Tester, Salesman extend from Employee, each class will implement method working() to do the specific job.

 

Open-Closed Principle (OCP)


It states that:

"Objects or entities should be open for extension, but closed for modification."


It means when we want to add/change attributes, functions for a class, we should consider creating a new class extending the old class than editing that old one.


For example, we have class Animal with method makeSound():

In case we have other animals such as "snake", "horse", we have to modify the switch of method makeSound(). When the application grows and becomes complex, you will see that the switch statement would be repeated over and over again in makeSound() each time a new animal is added, all over application.

The function makeSound() does not conform to the OCP because it cannot be closed against new kinds of animals.

Solution:

Now, if we add a new kind of animal, makeSound() function of class Animal doesn't need to change. All we need to do is create a new class and override makeSound() function.

 

Liskov Substitution Principle (LSP)


It states that:

"If S is a subtype of T, then objects of type T may be replaced (or substituted) with objects of type S."


This can be formulated mathematically as:

  • Let q(x) be a property provable about objects x of type T.

  • Then q(y) should be true for objects y of type S, where S is a subtype of T.

More generally it states that objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.

An example that violates LSP:

  • We have class Animal, which contains method countLegs().

  • Then we have a class Lion extending class Animal, Lion can use countLegs().

  • But when we have a class Snake extending class Animal, this causes the correctness of class Animal since Snake has no legs.

 

Interface Segregation Principle (ISP)


It states that:

"A client should never be forced to implement an interface that it doesn't use or clients shouldn't be forced to depend on methods they do not use."


This principle deals with the disadvantages of implementing big interfaces. In other words, it is better to have many smaller interfaces, than fewer, fatter interfaces.


We should not add additional functionality to an existing interface by adding new methods. Instead, create a new interface and let your class implement multiple interfaces if needed.


Imagine that we have a massive interface with over 100 methods. Implementing those methods in the child class will be immensely struggling. Even in most cases, child class does not use that implemented methods.

Solution:

  • Divide big interfaces into smaller interfaces.

  • The child class can implement many small interfaces in need.

 

Dependency Inversion Principle (DIP)


The last, but definitely not the least states that:

"Entities must depend on abstractions, not on concretions. It states that the high-level module must not depend on the low-level module, but they should depend on abstractions."


To be simpler:

  1. Dependency should be on abstractions not concretions

  2. High-level modules should not depend upon low-level modules. Both should depend upon abstractions.

  3. Abstractions should not depend on details. Details should depend upon abstractions.

To comply with this principle, we need to use a design pattern known as a dependency inversion pattern, most often solved by using dependency injection. We'll have another post for these.


Typically, dependency injection is used simply by 'injecting' any dependencies of a class through the class's constructor as an input parameter.


An example of violating DIP:

First, the MySQLConnection is the low-level module while the PasswordReminder is high-level, but according to DIP, this violates principle since the high-level module (PasswordReminder) is being enforced to depend on the low-level module (MySQLConnection).


Later if we want to change the database engine to (NoSQL, MongoDB, so on...), we have also to edit PasswordReminder and thus violate OCP too.


The PasswordReminder should not care what database your application uses, since high-level and low-level modules should depend on abstraction, we can create an interface:

The interface has a connect method and the MySQLConnection class implements this interface, also instead of directly type-hinting MySQLConnection class in the constructor of the PasswordReminder, we instead type-hint the interface and no matter the type of database your application uses, the PasswordReminder class can easily connect to the database without any problems.

According to the example above, you can now see that the high-level module does not depend on the low-level module, but it depends on abstraction. So does the low-level module.

 

Conclusion


By applying these 5 principles that make the S.O.L.I.D acronym, we can get benefit from a reusable, maintainable, scalable and easy testable codebase. They might seem to be a handful at first, but with continuous usage and adherence to its guidelines, it'll become a part of you and your code.


These principles used by professional software engineers all around the world, and if you are serious about creating ‘solid’ software, you should start applying these principles now.

773 views2 comments

2 Comments


Tuyen Nguyen
Tuyen Nguyen
Nov 24, 2019

Thank you!!! Have any question?

Like

vothituongvy105
Nov 24, 2019

Excellent!!!

Like
bottom of page