SOLID is an acronym for the five main principles of Object-Oriented Programming (OOP). It will make the OOP Designs more maintainable, understandable, and flexible. Adopting this SOLID practice in projects will give many benefits like avoiding code smells, refactoring codes, etc. In this blog, we will see all 5 principles of Object-oriented design in detail
Table of Content
Introduction
Alan Kay circa 1966 or 1967 coined Object-Oriented Programming” (OOP) while he was at grad school. Ivan Sutherland’s seminal Sketchpad application was an early inspiration for OOP. Object-oriented programming (OOP) is a programming paradigm based on the concept of objects, which are data structures that contain data, in the form of fields (or attributes) and code, in the form of procedures, (or methods).
SOLID Explanation
- S – Single-responsibility Principle
- O – Open-closed Principle
- L – Liskov Substitution Principle
- I – Interface Segregation Principle
- D – Dependency Inversion Principle
Single-Responsibility Principle
In the normal generation, we say that The single-responsibility principle is a computer programming principle that states that “A module should be responsible to one, and only one, actor.” The term actor refers to a group that requires a change in the module.
The idea behind the single-Responsibility principle is that every class, module, or function in a program should have one responsibility/purpose in a program. As a commonly used definition, “every class should have only one reason to change”.
Create the shape classes and have the constructors set up the required parameters.
For a Square shape, you need to know the length of the square
class Square
{
public $length;
public function construct($length)
{
$this->length = $length;
}
}
For Circle, you need to know the radius
class Circle
{
public $radius;
public function construct($radius)
{
$this->radius = $radius;
}
}
To calculate the area, create AreaCalculator class and write the code, to sum up, the areas of the given shapes. First, to find the area of a square, we have to square the length. To find the area of a circle, multiply the squared radius by pi
class AreaCalculator
{
protected $shapes;
public function __construct($shapes = [])
{
$this->shapes = $shapes;
}
public function sum()
{
foreach ($this->shapes as $shape) {
if (is_a($shape, 'Square')) {
$area[] = pow($shape->length, 2);
} elseif (is_a($shape, 'Circle')) {
$area[] = pi() * pow($shape->radius, 2);
}
}
return array_sum($area);
}
public function output()
{
return implode('', [
'',
'Sum of the areas of provided shapes: ',
$this->sum(),
'',
]);
}
}
To use the above AreaCalculator class, we have to initiate the class and pass the array of shapes. Let’s see this with three different shapes
$shapes = [
new Circle(2),
new Square(5),
new Square(6),
];
$areas = new AreaCalculator($shapes);
echo $areas->output();
The problem with this is, the AreaCalculator will handle only the output of logic. If a user wants their output in a different format like JSON, then it will violate the Single-responsibility principle. Because AreaCalculator will work only for output and won’t care for the format of the output. So to handle this, we can use SumCalculatorOutputter for the output
class SumCalculatorOutputter
{
protected $calculator;
public function __constructor(AreaCalculator $calculator)
{
$this->calculator = $calculator;
}
public function JSON()
{
$data = [
'sum' => $this->calculator->sum(),
];
return json_encode($data);
}
public function HTML()
{
return implode('', [
'',
'Sum of the areas of provided shapes: ',
$this->calculator->sum(),
'',
]);
}
}
SumCalculatorOutputter class would work like this
$shapes = [
new Circle(2),
new Square(5),
new Square(6),
];
$areas = new AreaCalculator($shapes);
$output = new SumCalculatorOutputter($areas);
echo $output->JSON();
echo $output->HTML();
Do like this It all satisfies the single-responsibility principle of the OOP
Open-Closed Principle
The Open-Close principle (OCP) is the O in the well-known SOLID acronym. A module will be said to be open if it is still available for extension. For example, it should be possible to add fields to the data structures it contains, or new elements to the set of functions it performs.
Two intents of Open Closed Principle state that we should try not to alter existing code while adding new functionalities. It means that existing code should be open for extension and closed for modification(unless there is a bug in existing code).
Let’s look again into the AreaCalculator class and focus on the sum method:
class AreaCalculator
{
protected $shapes;
public function __construct($shapes = [])
{
$this->shapes = $shapes;
}
public function sum()
{
foreach ($this->shapes as $shape) {
if (is_a($shape, 'Square')) {
$area[] = pow($shape->length, 2);
} elseif (is_a($shape, 'Circle')) {
$area[] = pi() * pow($shape->radius, 2);
}
}
return array_sum($area);
}
}
If we want to sum the area of a triangle, hexagon, or any other shape, we have to edit the AreaCalculator code and add if/else. We can’t do this all the time, so to handle this we have to remove the logic of finding the area of shape from AreaCalculator and add it to each shape class
Here is the area defined in class Square
class Square
{
public $length;
public function __construct($length)
{
$this->length = $length;
}
public function area()
{
return pow($this->length, 2);
}
}
Here is the area defined in class Circle
class Circle
{
public $radius;
public function construct($radius)
{
$this->radius = $radius;
}
public function area()
{
return pi() * pow($shape->radius, 2);
}
}
Then the class AreaCalculator will become like this
class AreaCalculator
{
// ...
public function sum()
{
foreach ($this->shapes as $shape) {
$area[] = $shape->area();
}
return array_sum($area);
}
}
Now, we can create another shape class and pass it in when calculating the sum without breaking the code. So it doesn’t violate the Open-closed principle. But how can we find if the object passed into the AreaCalculator class is a shape or if it has the area method?
To solve this, we need to create a ShapeInterface to support the area method
interface ShapeInterface
{
public function area();
}
ShapeInterface in shape classes
Here is the update to the Square class
class Square implements ShapeInterface
{
// ...
}
Here is the update to the Circle class
class Circle implements ShapeInterface
{
// ...
}
Tn sum method of AreaCalculator, we can check whether the shapes are instances of ShapeInterface; otherwise, throw an exception
class AreaCalculator
{
// ...
public function sum()
{
foreach ($this->shapes as $shape) {
if (is_a($shape, 'ShapeInterface')) {
$area[] = $shape->area();
continue;
}
throw new AreaCalculatorInvalidShapeException();
}
return array_sum($area);
}
}
Follow this to complete the open-closed principle.
Liskov Substitution Principle
Simply put, the Liskov Substitution Principle (LSP) states that objects of a superclass should be replaceable with objects of its subclasses without breaking the application. In other words, we want to have the objects of our subclasses behaving the same way as the objects of our superclass.
Mainly Liskov Substitution Principle helps us model good inheritance hierarchies. It helps us prevent model hierarchies that don’t conform to the Open/Closed principle. So any inheritance model that adheres to the Liskov Substitution Principle will implicitly follow the Open/Closed principle.
Let’s work on the example of AreaCalculator class, consider a new VolumeCalculator class that extends the AreaCalculator class:
class VolumeCalculator extends AreaCalculator
{
public function construct($shapes = [])
{
parent::construct($shapes);
}
public function sum()
{
// logic to calculate the volumes and then return an array of output
return [$summedData];
}
}
The SumCalculatorOutputter class resembles this:
class SumCalculatorOutputter {
protected $calculator;
public function __constructor(AreaCalculator $calculator) {
$this->calculator = $calculator;
}
public function JSON() {
$data = array(
'sum' => $this->calculator->sum();
);
return json_encode($data);
}
public function HTML() {
return implode('', array(
'',
'Sum of the areas of provided shapes: ',
$this->calculator->sum(),
''
));
}
}
Let’s run one example with this
$areas = new AreaCalculator($shapes);
$volumes = new VolumeCalculator($solidShapes);
$output = new SumCalculatorOutputter($areas);
$output2 = new SumCalculatorOutputter($volumes);
The VolumeCalculator class sum method, return $summedData:
class VolumeCalculator extends AreaCalculator
{
public function construct($shapes = [])
{
parent::construct($shapes);
}
public function sum()
{
// logic to calculate the volumes and then return a value of output
return $summedData;
}
}
It can satisfy the Liskov substitution principle.
Interface Segregation Principle
In software engineering, the interface segregation principle (ISP) states that no code should be forced to depend on methods it does not use. So ISP splits interfaces that are very large into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them.
Interface segregation principle states: 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.
So we need this principle because Clients should not be forced to depend upon interfaces that they do not use“. So this principle aims to reduce the side effects of using larger interfaces by breaking application interfaces into smaller ones.
The ShapeInterface to add another contract:
interface ShapeInterface
{
public function area();
public function volume();
}
For 2D shapes like squares and circles, we can’t do the volume method, because it will violate the Interface Segregation Principle. So instead of that, we can create ThreeDimensionalShapeInterface that has the volume contract and three-dimensional shapes:
interface ShapeInterface
{
public function area();
}
interface ThreeDimensionalShapeInterface
{
public function volume();
}
class Cuboid implements ShapeInterface, ThreeDimensionalShapeInterface
{
public function area()
{
// calculate the surface area of the cuboid
}
public function volume()
{
// calculate the volume of the cuboid
}
}
Single API for managing the shapes
interface ManageShapeInterface
{
public function calculate();
}
class Square implements ShapeInterface, ManageShapeInterface
{
public function area()
{
// calculate the area of the square
}
public function calculate()
{
return $this->area();
}
}
class Cuboid implements ShapeInterface, ThreeDimensionalShapeInterface, ManageShapeInterface
{
public function area()
{
// calculate the surface area of the cuboid
}
public function volume()
{
// calculate the volume of the cuboid
}
public function calculate()
{
return $this->area();
}
}
This method satisfies the interface segregation principle.
Dependency Inversion Principle
DIP is the idea that high-level modules/implementations should not depend on lower-level modules/implementations. So if that is the case — that our high-level modules depend on lower-level modules means that there is an inversion of dependencies! so the name of the principle.
(DIP) states that high-level modules should not depend on low-level modules; so both should depend on abstractions. Abstractions should not rely on details. Details should depend upon abstractions.
The dependency inversion principle helps us to couple software modules loosely. So the principle was arrived at after many years of coupling software modules, and it states that: So High-level modules should not import anything from low-level modules; they should both depend on abstractions.
PasswordReminder that connects to a MySQL database
class MySQLConnection
{
public function connect()
{
// handle the database connection
return 'Database connection';
}
}
class PasswordReminder
{
private $dbConnection;
public function __construct(MySQLConnection $dbConnection)
{
$this->dbConnection = $dbConnection;
}
}
So you can code to an interface since high-level and low-level modules should depend on abstraction:
interface DBConnectionInterface
{
public function connect();
}
* The PasswordReminder class can connect to the database without any problems and the open-close principle:
class MySQLConnection implements DBConnectionInterface
{
public function connect()
{
// handle the database connection
return 'Database connection';
}
}
class PasswordReminder
{
private $dbConnection;
public function __construct(DBConnectionInterface $dbConnection)
{
$this->dbConnection = $dbConnection;
}
}
It can establish that both the high-level and low-level modules depend on abstraction while using the correct coding like this.
Conclusion
Finally, now you are responsible to present the five principles of the SOLID Code. Try your best Projects that are based on SOLID principles can be surprised more while you are in programming and extended, modified, tested, and refactored with fewer complications over this article. Hope you found this blog helpful, follow Publish Square for more blogs like this