SOLID Principles: Every thing you need to know
The SOLID Principles are five principles that originally created for using in Object Oriented Programming (OOP), but over time, they were applied to higher level components, like architecture components.
The first time for SOLID Principles to mention was in Robert C. Martin paper Design Principles and Design Patterns, but he mentioned only four principles of them. After a while, at his book “Clean Architecture”, he mentioned them in their order that makes the word “Solid”, describing what will happen when you apply them. What will happen?! Your design will be SOLID.
The Principles are:
- Single Responsibility Principles (SRP)
- Open-Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
Let’s dig into every principle and illustrate them. We will be using PHP Code Samples, but if you are not familiar with php, don’t worry at all, it is simple and some notes will be stated if any code is unpopular for some folks of other programming languages.
Single Responsibility Principles (SRP)
Every class, module, or function in a program should be responsible to one, and only one actor.
When you work on any thing, focus on the idea of “Separation of Concerns”. Let’s have an example of a module that violates the principle.
You have a Student class, it has some attributes that indicates the students, like name, grade and university
class Student {
public $name;
public $grade;
public $university;public function __construct(string $n, int $g, string $uni)
{
$this->name = $n;
$this->grade = $g;
$this->university = $uni;
}
}
Note: PHP doesn’t require you to write the datatype of variables or parameters, but it is allowed, and I’ve written it for a specific purpose you will know in a while. The dollar sign ($) in php means this is a variable. The function __construct in php is the constructor.
It is a nice class and doesn’t have any problems (or does it?!), but what if you add some other attributes about the university itself?! Some attributes like university_name and university_location. Is it a problem?!
Of course it is. Because now this class is responsible for two actors, the Student, and the University. Suppose your system has an admin (or a developer) for every module, the Student and the University. Both of these actors will need to work on the same module. So, what can we do?!
We should separate each of the concerns of each module to a different classes.
class University {
public $name;
public $location;public function __construct($name, $location) {
$this->name = $name;
$this->location = $location;
}
}class Student {
public $name;
public $grade;
public $university;public function __construct(string $n, int $g, University $uni)
{
$this->name = $n;
$this->grade = $g;
$this->university = $uni;
}
}
Now, you should notice a very important change we made, the datatype of the parameter $uni. At the beginning it was just a string, knowing we will add other attributes that are completely related to the university, BUT implemented within the Student class. But now it is an object of the University class, and any change you want to make on the University logic, you will be going towards this very class.
With this type of separation, you guarantee an important thing, that is, no one will make a change on some other one’s work without him knowing, and changing his own work that depends on this module.
Open-Closed Principle (OCP)
A software module should be open for extension but closed for modification.
Modules in software should be built keeping in mind the idea of it will have some extensions, not modifications.
What is the difference?! Suppose you built a system, and the client want to add some features, all of them do this. Once he asks you for this, you will ADD some lines of code, not edit the already written codes.
A good example is the same code previously mentioned in this article.
class Student {
public $name;
public $grade;
public $university;public function __construct(string $n, int $g, string $uni)
{
$this->name = $n;
$this->grade = $g;
$this->university = $uni;
}
}
Suppose you built this code and you know and are sure that the client does not need any thing related to the university except for its name. But every client make a lot of additions after deployment, annoying thing but this is what happens.
Your client came to an idea to add a details of the university. You should have seen this and not get surprised, even if he told you that he needs only the name, but you know he will ask for more.
You should make another class for the university, even if it has only one attribute, which is its name
class University {
public $name;public function __construct($name) {
$this->name = $name;
}
}class Student {
public $name;
public $grade;
public $university;public function __construct(string $n, int $g, University $uni)
{
$this->name = $n;
$this->grade = $g;
$this->university = $uni;
}
}
You noticed it is the very same thing we did at the SRP, but now we did it for a different reason. Let’s assume the code that you WILL NEED to write AFTER he asks for the new feature.
class University {
public $name;
public $location;
public $date_of_foundation;
public $number_of_students;public function __construct($name) {
$this->name = $name;
// Assigning the other attributes
}
}class Student {
public $name;
public $grade;
public $university;public function __construct(string $n, int $g, University $uni)
{
$this->name = $n;
$this->grade = $g;
$this->university = $uni;
}
}
This is the right way to implement the new feature, how to come to this?!
If you write the first code, and you want to access the name of the university, you will write this:
$st = new Student();
echo $st->university;
Because the attribute $university is a string and you just need to print it. After the modification you will access the name writing this:
$st = new Student();
echo $st->university->name;
The name of the university may be accessed many times, so you will need to MODIFY EVERY time you write it.
Now suppose you have already implement the class University from the beginning?! You will be asked only to extend some attributes and methods to the class university. Do you need modify any code?! Absolutely not! It is terrific.
I think a good other illustration for the OCP is to use the foreign key in database for such an example of the Student and University, or I should say “users” and “universities” tables.
Liskov Substitution Principle (LSP)
Let Φ(x) be a property provable about objects x of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T.
I think the definition of the LSP might be the hardest definition at the first impression, but once you get into it, it becomes easier to understand.
Suppose you have two classes, let them be Parent and Child, and as you can guess, the Child class inherits from the Parent class. You have a method, let it be myMethod(), at the Parent class.
Now you have two objects, object p from class Parent, and object c from class Child. To access the method, you will simply call it using the object p, and you know exactly how to use it, and what should it do. But, what if you access it through the object c?! Will it make the same thing or it will break the program?!
The LSP states that it should work for the child object as same as it works for the parent child.
The famous example for this is the Rectangle & Square classes. Let’s get back in time to our math class when you learnt that every Square is a Rectangle, but both its sides are equal (the height and the width). Thus, we can let the class Square be a child of the class Rectangle.
class Rectangle
{
private $height;
private $width;
public function setHeight( $height ) {
$this->height = $height;
}
public function setWidth( $width ) {
$this->width = $width;
}
public function getHeight() {
return $this->height;
}
public function getWidth() {
return $this->width;
} public function getArea() {
return $this->height * $this->width;
}
}
class Square extends Rectangle
{
public function setWidth( $width ) {
$this->width = $width;
$this->height = $width;
}
public function setHeight( $height ) {
$this->height = $height;
$this->width = $height;
}
}
Sounds like every thing is terrific. We made a little override at the class Square to guarantee that both sides are always equal each other.
Now, let’s consider this function:
function printArea(Rectangle $shape) {
$shape->setHeight(300);
$shape->setHeight(200);
echo $shape->getArea();
}
Can you tell me exactly what is the output?! Using simple calculation you would say it is absolutely “60000”. Mmmmm, you are right, AND WRONG.
If the argument of the function is an object of the class Rectangle, then you are right. But what if it is an object of the class Square?!
The last called function setHight() will be implemented in a different way resulting a different output. So, mathematically, Square is a Rectangle, but when you apply LSP, you will find a difference in using them.
So, the simple solution is to separate these two classes, and you will love to make both of them inherit from the class Shape.
Where is the class Shape?! We will implement it at the next principles.
Interface Segregation Principle (ISP)
Clients should not be forced to depend upon interfaces that they do not use.
While designing your OO program, you should be aware of a difference between the abstract class and the interface. Any class can extends one, and only one, abstract class, while it can implements interfaces as many as you need. That means you can easily put your logic in many interfaces.
The idea behind the ISP is to design your interfaces putting the necessary, and only the necessary, methods within the interface, otherwise, separate them into difference interfaces.
Let’s get back to our previous example of shapes. We said we would make an abstract class Shape, but for some reason I will put most of its abstract methods in an interface.
abstract class Shape {
private $color;
public function setColor( $color ) {
$this->color = $color;
}
public function getColor() {
return $this->color;
}
}
interface IShape {
function getArea();
function getCircumference();
}
class Square extends Shape implements IShape
{
private $length;
public function getLength() {
return $this->length;
}
public function setLength( $length ) {
$this->length = $length;
}
public function getArea() {
return $this->length * $this->length;
}
function getCircumference() {
return $this->length * 4;
}
}
class Rectangle extends Shape implements IShape
{
public $height;
public $width;
public function setHeight( $height ) {
$this->height = $height;
}
public function setWidth( $width ) {
$this->width = $width;
}
public function getHeight() {
return $this->height;
}
public function getWidth() {
return $this->width;
}
public function getArea() {
return $this->height * $this->width;
}
public function getCircumference() {
return ($this->height + $this->width) * 2;
}
}
As you can see, every thing is working well, and every concrete class implements exactly what you need. Now we need to add more shapes like the ball and Cube. Cube (or any 3D shape) does not have a circumference, indeed it has a volume. We can add getVolume() to our interface. This is exactly how we violate the ISP. Let’s see
abstract class Shape {
private $color;
public function setColor( $color ) {
$this->color = $color;
}
public function getColor() {
return $this->color;
}
}
interface IShape {
function getArea();
function getCircumference();
function getVolume();
}
class Square extends Shape implements IShape
{
private $length;
public function getArea() {
return $this->length * $this->length;
}
function getCircumference() {
return $this->length * 4;
}
function getVolume() {
return 0;
}
}
class Rectangle extends Shape implements IShape
{
public $height;
public $width;
public function getArea() {
return $this->height * $this->width;
}
public function getCircumference() {
return ($this->height + $this->width) * 2;
}
function getVolume() {
return 0;
}
}
class Cube extends shape implements IShape {
private $length;
public function getArea() {
return $this->length * 4 * 6;
}
function getCircumference() {
return 0;
}
function getVolume() {
return $this->length * $this->length * $this->length;
}
}
Notice the bolded lines of code. What does it mean to return 0?! What does it mean for a cube to have a circumference or a square to have a volume?! In fact, it does not mean any thing, you just forced yourself to implement some methods that you don’t need at all.
The solution is to separate the methods of the 2D Shapes and 3D Shapes into two different interfaces, while the common methods should be kept in the abstract class:
abstract class Shape {
private $color;
public abstract function getArea();
}
interface I2DShape {
function getCircumference();
}
interface I3DShape {
function getVolume();
}
class Square extends Shape implements I2DShape
{
private $length;
public function getArea() {
return $this->length * $this->length;
}
function getCircumference() {
return $this->length * 4;
}
}
class Rectangle extends Shape implements I2DShape
{
public $height;
public $width;
public function getArea() {
return $this->height * $this->width;
}
public function getCircumference() {
return ($this->height + $this->width) * 2;
}
}
class Cube extends shape implements I3DShape {
private $length;
public function getArea() {
return $this->length * 4 * 6;
}
function getVolume() {
return $this->length * $this->length * $this->length;
}
}
As you noticed, the class Square does not have the function getVolume() any more, why?! Because it does not need it at all.
Dependency Inversion Principle (DIP)
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.
The concept of High-Level is a very common concept, and very important. For simplifying it, let’s call the abstract class High-Level class, and the concrete classes Low-Level. That means high level modules sometimes does not have all the business logic within it.
What is dependency?! Assume you have a University class, and Student class. The Student class has an attribute of type University. Can you implement the Student class without using the University class at all?! Of course you need to implement the University class first and include (import) it. That means Student class is Dependent on the University class.
Let’s assume you build a function that make some calculations dependent on the Square Area
function getDoubleArea(Square $s) {
$area = $s->getArea();
$result = $area * 2;
return $result;
}
It’s very simple and you can see that no problems are there. But there is a very better approach. Now your function is dependent on the Square class, which is low-level module. What about implementing it dependent on the Shape class, which is High Level module?!
function getDoubleArea(Shape $s) {
$area = $s->getArea();
$result = $area * 2;
return $result;
}
It is a very small change, but it has a great impact. Now your function can use any type of shape such as Square, Rectangle, Circle, Ball or Cube. Why is that?! Because it is dependent on High-Level Module.
In the same way, you should build your classes, functions or modules depending on High-Level modules, as it lets apply a great flexibility and reusability.