SOLID stands for 5 object-oriented design (OOP) principles. A project that applies these principles will have code that is easy to read, test, and clear. And above all, it helps dev easily maintain and develop the project, specifically as follows:
- S: S ingle-responsibility principle
- O: O pen-closed principle
- L: L iskov substitution principle
- I: I nterface segregation principle
- D: D ependency Inversion Principle
Ok, now let me learn more about this principle!
S – Single Responsibility Principle (SRP)
A class should only handle only one job (Only one class can be modified for a single reason).
- If the code violates this principle – a class handles many tasks:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <?php namespace Demo; use DB; class OrdersReport { public function getOrdersInfo($startDate, $endDate) { $orders = $this->queryDBForOrders($startDate, $endDate); return $this->format($orders); } protected function queryDBForOrders($startDate, $endDate) { return DB::table('orders')->whereBetween('created_at', [$startDate, $endDate])->get(); } protected function format($orders) { return '<h1>Orders: ' . $orders . '</h1>'; } } |
The OrdersReport class is doing three jobs: retrieving the Order information, reading the Order data from the DB and displaying the Order. When changing the DB or changing how to get data, …, it is imperative that we modify the class. Later, the class will become bigger and bigger, leading to difficult to maintain, difficult to understand, and difficult to clear functions.
When the requirements change, the code refactors, meaning the class is modified. The more tasks a class processes, the more changes are required, and the more difficult those changes will be. So we need to separate the class into separate classes to handle each job.
In this example we need to split as follows
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | <?php namespace Report; use ReportRepositoriesOrdersRepository; class OrdersReport { protected $repo; protected $formatter; public function __construct(OrdersRepository $repo, OrdersOutPutInterface $formatter) { $this->repo = $repo; $this->formatter = $formatter; } public function getOrdersInfo($startDate, $endDate) { $orders = $this->repo->getOrdersWithDate($startDate, $endDate); return $this->formatter->output($orders); } } namespace Report; interface OrdersOutPutInterface { public function output($orders); } namespace Report; class HtmlOutput implements OrdersOutPutInterface { public function output($orders) { return '<h1>Orders: ' . $orders . '</h1>'; } } namespace ReportRepositories; use DB; class OrdersRepository { public function getOrdersWithDate($startDate, $endDate) { return DB::table('orders')->whereBetween('created_at', [$startDate, $endDate])->get(); } } |
O – Open-Closed Principle
You can extend the class without modifying it inside.
- This principle is the foundation for building code that is easily maintainable and reusable.
In this shortcut when there is a new request, we can create a new class to extend (by inheriting or owning) the old class without modifying the old class. Let me see the following example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | <?php class Rectangle { public $width; public $height; public function __construct($width, $height) { $this->width = $width; $this->height = $height; } } class Circle { public $radius; public function __construct($radius) { $this->radius = $radius; } } class CostManager { public function calculate($shape) { $costPerUnit = 1.5; if ($shape instanceof Rectangle) { $area = $shape->width * $shape->height; } else { $area = $shape->radius * $shape->radius * pi(); } return $costPerUnit * $area; } } $circle = new Circle(5); $rect = new Rectangle(8,5); $obj = new CostManager(); echo $obj->calculate($circle); |
When we want to calculate the area of a circle, if we change the calculate method in the CostManager class. It will break the Open-closed principle. According to this principle, we cannot modify it, we need to expand it. So how to solve this problem, please follow the code below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | <?php interface AreaInterface { public function calculateArea(); } class Rectangle implements AreaInterface { public $width; public $height; public function __construct($width, $height) { $this->width = $width; $this->height = $height; } public function calculateArea(){ $area = $this->height * $this->width; return $area; } } class Circle implements AreaInterface { public $radius; public function __construct($radius) { $this->radius = $radius; } public function calculateArea(){ $area = $this->radius * $this->radius * pi(); return $area; } } class CostManager { public function calculate(AreaInterface $shape) { $costPerUnit = 1.5; $totalCost = $costPerUnit * $shape->calculateArea(); return $totalCost; } } $circle = new Circle(5); $obj = new CostManager(); echo $obj->calculate($circle); |
It is now possible to calculate the area without modifying the CostManager class.
L – Liskov Substitution Principle
This principle states that objects must be replaced by an instance of the subtype without changing the proper functioning of the system function.
Imagine you manage two types of coffee machines. According to the user plan, we will use the basic or premium coffee maker, the only difference is that the high-end machine produces better vanilla coffee than the basic machine. The main program behavior must be the same for both machines
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | <?php interface CoffeeMachineInterface { public function brewCoffee($selection); } class BasicCoffeeMachine implements CoffeeMachineInterface { public function brewCoffee($selection) { switch ($selection) { case 'ESPRESSO': return $this->brewEspresso(); default: throw new CoffeeException('Selection not supported'); } } protected function brewEspresso() { // Brew an espresso... } } class PremiumCoffeeMachine extends BasicCoffeeMachine { public function brewCoffee($selection) { switch ($selection) { case 'ESPRESSO': return $this->brewEspresso(); case 'VANILLA': return $this->brewVanillaCoffee(); default: throw new CoffeeException('Selection not supported'); } } protected function brewVanillaCoffee() { // Brew a vanilla coffee... } } function getCoffeeMachine(User $user) { switch ($user->getPlan()) { case 'PREMIUM': return new PremiumCoffeeMachine(); case 'BASIC': default: return new BasicCoffeeMachine(); } } function prepareCoffee(User $user, $selection) { $coffeeMachine = getCoffeeMachine($user); return $coffeeMachine->brewCoffee($selection); } |
I – Interface Segregation Principle
Do not use large interfaces, but instead split into smaller interfaces, with specific purposes.
This principle allows the development of specific interfaces instead of generic interfaces.
Imagine, in the future, you will create a FutureCar that can run and fly (imagine a little high), as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | <?php interface VehicleInterface { public function drive(); public function fly(); } class FutureCar implements VehicleInterface { public function drive() { echo 'Driving a future car!'; } public function fly() { echo 'Flying a future car!'; } } class Car implements VehicleInterface { public function drive() { echo 'Driving a car!'; } public function fly() { throw new Exception('Not implemented method'); } } class Airplane implements VehicleInterface { public function drive() { throw new Exception('Not implemented method'); } public function fly() { echo 'Flying an airplane!'; } } |
The main problem here you can see is that Car and Airplane have the above methods but cannot be used again. The solution is to divide VehicleInterface into two more specific interfaces, which are used only when needed, as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | <?php interface CarInterface { public function drive(); } interface AirplaneInterface { public function fly(); } class FutureCar implements CarInterface, AirplaneInterface { public function drive() { echo 'Driving a future car!'; } public function fly() { echo 'Flying a future car!'; } } class Car implements CarInterface { public function drive() { echo 'Driving a car!'; } } class Airplane implements AirplaneInterface { public function fly() { echo 'Flying an airplane!'; } } |
D – Dependency Inversion Principle
A. High level modules should not depend on low level modules. Both should depend on abstractions. B. Abstractions should not depend on details. Details should depend on abstractions. Robert C. Martin
This principle allows programmers to write code separately and easily reuse in many places.
Consider the following example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?php class UserDB { private $dbConnection; public function __construct(MySQLConnection $dbConnection) { $this->$dbConnection = $dbConnection; } public function store(User $user) { // Store the user into a database... } } |
In this case, the UserDB class depends directly from the MySQL database. That means if we change the database engine we are using, we need to rewrite this class and this will violate the Open-Closed principle.
The solution is to develop abstraction of database connectivity:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <?php interface DBConnectionInterface { public function connect(); } class MySQLConnection implements DBConnectionInterface { public function connect() { // Return the MySQL connection... } } class UserDB { private $dbConnection; public function __construct(DBConnectionInterface $dbConnection) { $this->$dbConnection = $dbConnection; } public function store(User $user) { // Store the user into a database... } } |
Benefit
Following the SOLID Principles gives us many benefits, they make our system reusable, re-structured, easy to maintain, expand, develop and more.
Above is an article explaining SOLID principles I refer to from articles: SOLID Principles made easy by Dhurim Kelmendi and SOLID Principles Simplified with Examples in PHP by Ideneal.
Hopefully the article will help you improve the quality of your coding process.