SOLID Crib with PHP Examples
The subject of SOLID principles and, in general, code cleanliness has been raised more than once on Habré, and, perhaps, it has already been rather well-traveled. But nevertheless, not so long ago I had to go through interviews to an interesting IT company, where I was asked to talk about the principles of SOLID with examples and situations where I did not comply with these principles and what it led to. And at that moment I realized that at some subconscious level I understand these principles and can even call them all, but to give concise and understandable examples for me became a problem. Therefore, I decided for myself and for the community to generalize information on SOLID principles for an even better understanding of it. The article should be useful for people who are only familiar with SOLID principles, as well as for people who "ate a dog" on SOLID principles.
For those who are familiar with the principles and only want to refresh the memory of them and their use, you can turn right to the cheat sheet at the end of the article.
What are SOLID principles? According to the definition of Wikipedia, this is:
Thus, we have 5 principles, which we will consider below:
So, as an example, let's take a rather popular and widely used example - an online store with orders, goods and customers.
The principle of sole responsibility states: “One single responsibility must be assigned to each object . ” Those. in other words, a particular class must solve a specific problem — neither more nor less.
Consider the following class description for presenting an order in an online store:
As you can see, this class performs operations for 3 different types of tasks: working with the order itself (
What can this lead to?
This leads to the fact that if we want to make changes to the printing methods or the operation of the store, we change the class of the order itself, which can lead to its inoperability.
This problem can be solved by dividing this class into 3 separate classes, each of which will do its job
Now each class is engaged in its own specific task and for each class there is only 1 reason for changing it.
This principle reads -
Consider the class example
In this case, the repository is the database. e.g. MySQL. But suddenly we wanted to load our order data, for example, through the API of a third-party server, which, for example, takes data from 1C. What changes will we need to make? There are several options, for example, directly changing the methods of the class
Thus, we can change the source and, accordingly, the behavior for the class
Perhaps the principle that causes the greatest difficulties in understanding.
The principle says - "Objects in the program can be replaced by their heirs without changing the properties of the program . " In my own words, I would say this - when using the class inheritor, the result of code execution should be predictable and not change the properties of the method.
Unfortunately, I could not come up with an accessible example for this principle in the framework of the online store, but there is a classic example with a hierarchy of geometric shapes and area calculation. Example code below.
Obviously, such code is clearly not executed as expected from it.
But what is the problem? Isn't a “square” a “rectangle”? It is, but in geometric terms. In terms of objects, a square is not a rectangle, since the behavior of the “square” object is not consistent with the behavior of the “rectangle” object.
Then how to solve the problem?
The solution is closely related to such a concept as design by contract . A description of design by contract may take more than one article, so we restrict ourselves to the features that relate to the Lisk principle .
Contract designing leads to some restrictions on how contracts can interact with inheritance, namely:
“What other pre- and postconditions?” - you may ask.
Answer : preconditions are what must be performed by the caller before calling the method, postconditions are what are guaranteed by the called method.
Let us return to our example and see how we changed the pre- and postconditions.
We did not use any preconditions when calling the methods for setting the height and width, but we changed the postconditions in the successor class and changed to weaker ones, which was impossible according to the Liskov principle.
That’s why we weakened them. If we
Therefore, it is better not to make the “square” hierarchy inherit the “rectangle” within the framework of OOP and the task of calculating the area of a figure, but to make them as 2 separate entities:
A good real example of non-observance of the Liskow principle and the decision taken in this regard is considered in Robert Martin's book “Rapid Program Development” in the section “Liskow Substitution Principle. A real example. "
This principle states that “Many specialized interfaces are better than one universal”.
Observance of this principle is necessary so that client classes using / implementing the interface know only about the methods that they use, which leads to a reduction in the amount of unused code.
Let us return for example with the online store.
Suppose our products may have a promotional code, a discount, they have some kind of price, condition, etc. If this is clothing, then it is set for what material it is made of, color and size.
We describe the following interface
This interface is bad in that it includes too many methods. But what if our class of goods cannot have discounts or promotional codes, or it makes no sense for it to establish the material from which it is made (for example, for books). Thus, in order not to implement unused methods in each class, it is better to split the interface into several smaller ones and implement the required interfaces with each specific class.
The principle reads: “Dependencies within the system are built on the basis of abstractions. Top-level modules are independent of lower-level modules. Abstractions should not depend on the details. Details must depend on abstractions . ” This definition can be shortened - "dependencies should be built with respect to abstractions, not details . "
For example, consider the payment of an order by a buyer.
Everything seems quite logical and logical. But there is one problem - the class
In order to get rid of dependence on a particular class, it is necessary to make it so
Thus, the class
Summarizing all of the above, I would like to make the following cheat sheet
I hope my cheat sheet will help someone in understanding the principles of SOLID and give impetus to their use in their projects.
Thanks for attention.
PS In the comments advised a good book - Robert Martin "Rapid software development." There, SOLID principles are described in great detail and with examples.
For those who are familiar with the principles and only want to refresh the memory of them and their use, you can turn right to the cheat sheet at the end of the article.
What are SOLID principles? According to the definition of Wikipedia, this is:
an abbreviation of the five basic principles of class design in object-oriented design - S ingle responsibility, O pen-closed, L iskov substitution, I nterface segregation and D ependency inversion.
Thus, we have 5 principles, which we will consider below:
- Single responsibility principle
- The principle of openness / closure (Open-closed)
- The principle of substitution Barbara Liskov (Liskov substitution)
- Interface segregation
- Dependency Invertion Principle
Single responsibility principle
So, as an example, let's take a rather popular and widely used example - an online store with orders, goods and customers.
The principle of sole responsibility states: “One single responsibility must be assigned to each object . ” Those. in other words, a particular class must solve a specific problem — neither more nor less.
Consider the following class description for presenting an order in an online store:
class Order
{
public function calculateTotalSum(){/*...*/}
public function getItems(){/*...*/}
public function getItemCount(){/*...*/}
public function addItem($item){/*...*/}
public function deleteItem($item){/*...*/}
public function printOrder(){/*...*/}
public function showOrder(){/*...*/}
public function load(){/*...*/}
public function save(){/*...*/}
public function update(){/*...*/}
public function delete(){/*...*/}
}
As you can see, this class performs operations for 3 different types of tasks: working with the order itself (
calculateTotalSum, getItems, getItemsCount, addItem, deleteItem
), displaying the order ( printOrder, showOrder
) and working with the data warehouse ( load, save, update, delete
). What can this lead to?
This leads to the fact that if we want to make changes to the printing methods or the operation of the store, we change the class of the order itself, which can lead to its inoperability.
This problem can be solved by dividing this class into 3 separate classes, each of which will do its job
class Order
{
public function calculateTotalSum(){/*...*/}
public function getItems(){/*...*/}
public function getItemCount(){/*...*/}
public function addItem($item){/*...*/}
public function deleteItem($item){/*...*/}
}
class OrderRepository
{
public function load($orderID){/*...*/}
public function save($order){/*...*/}
public function update($order){/*...*/}
public function delete($order){/*...*/}
}
class OrderViewer
{
public function printOrder($order){/*...*/}
public function showOrder($order){/*...*/}
}
Now each class is engaged in its own specific task and for each class there is only 1 reason for changing it.
The principle of openness / closure (Open-closed)
This principle reads -
"программные сущности должны быть открыты для расширения, но закрыты для модификации"
. In simpler words, this can be described as follows - all classes, functions, etc. should be designed so that to change their behavior, we do not need to change their source code. Consider the class example
OrderRepository
.class OrderRepository
{
public function load($orderID)
{
$pdo = new PDO($this->config->getDsn(), $this->config->getDBUser(), $this->config->getDBPassword());
$statement = $pdo->prepare('SELECT * FROM `orders` WHERE id=:id');
$statement->execute(array(':id' => $orderID));
return $query->fetchObject('Order');
}
public function save($order){/*...*/}
public function update($order){/*...*/}
public function delete($order){/*...*/}
}
In this case, the repository is the database. e.g. MySQL. But suddenly we wanted to load our order data, for example, through the API of a third-party server, which, for example, takes data from 1C. What changes will we need to make? There are several options, for example, directly changing the methods of the class
OrderRepository
, but this does not correspond to the principle of openness / closure , since the class is closed for modification, and making changes to an already well-functioning class is undesirable. This means that you can inherit from the class OrderRepository
and override all methods, but this solution is not the best, since when you add a method to OrderRepository
us, you will have to add similar methods to all its heirs. Therefore, to fulfill the principle of openness / closenessit is better to apply the following solution - create an interface IOrderSource
that will be implemented by the corresponding classes MySQLOrderSource
, ApiOrderSource
and so on.IOrderSource interface and its implementation and use
class OrderRepository
{
private $source;
public function setSource(IOrderSource $source)
{
$this->source = $source;
}
public function load($orderID)
{
return $this->source->load($orderID);
}
public function save($order){/*...*/}
public function update($order){/*...*/}
}
interface IOrderSource
{
public function load($orderID);
public function save($order);
public function update($order);
public function delete($order);
}
class MySQLOrderSource implements IOrderSource
{
public function load($orderID);
public function save($order){/*...*/}
public function update($order){/*...*/}
public function delete($order){/*...*/}
}
class ApiOrderSource implements IOrderSource
{
public function load($orderID);
public function save($order){/*...*/}
public function update($order){/*...*/}
public function delete($order){/*...*/}
}
Thus, we can change the source and, accordingly, the behavior for the class
OrderRepository
by setting the class we need that implements IOrderSource
, without changing the class OrderRepository
.The principle of substitution Barbara Liskov (Liskov substitution)
Perhaps the principle that causes the greatest difficulties in understanding.
The principle says - "Objects in the program can be replaced by their heirs without changing the properties of the program . " In my own words, I would say this - when using the class inheritor, the result of code execution should be predictable and not change the properties of the method.
Unfortunately, I could not come up with an accessible example for this principle in the framework of the online store, but there is a classic example with a hierarchy of geometric shapes and area calculation. Example code below.
An example of a hierarchy of a rectangle and a square and calculating their area
class Rectangle
{
protected $width;
protected $height;
public setWidth($width)
{
$this->width = $width;
}
public setHeight($height)
{
$this->height = $height;
}
public function getWidth()
{
return $this->width;
}
public function getHeight()
{
return $this->height;
}
}
class Square extends Rectangle
{
public setWidth($width)
{
parent::setWidth($width);
parent::setHeight($width);
}
public setHeight($height)
{
parent::setHeight($height);
parent::setWidth($height);
}
}
function calculateRectangleSquare(Rectangle $rectangle, $width, $height)
{
$rectangle->setWidth($width);
$rectangle->setHeight($height);
return $rectangle->getHeight * $rectangle->getWidth;
}
calculateRectangleSquare(new Rectangle, 4, 5); // 20
calculateRectangleSquare(new Square, 4, 5); // 25 ???
Obviously, such code is clearly not executed as expected from it.
But what is the problem? Isn't a “square” a “rectangle”? It is, but in geometric terms. In terms of objects, a square is not a rectangle, since the behavior of the “square” object is not consistent with the behavior of the “rectangle” object.
Then how to solve the problem?
The solution is closely related to such a concept as design by contract . A description of design by contract may take more than one article, so we restrict ourselves to the features that relate to the Lisk principle .
Contract designing leads to some restrictions on how contracts can interact with inheritance, namely:
- Preconditions cannot be strengthened in a subclass.
- Postconditions cannot be relaxed in a subclass.
“What other pre- and postconditions?” - you may ask.
Answer : preconditions are what must be performed by the caller before calling the method, postconditions are what are guaranteed by the called method.
Let us return to our example and see how we changed the pre- and postconditions.
We did not use any preconditions when calling the methods for setting the height and width, but we changed the postconditions in the successor class and changed to weaker ones, which was impossible according to the Liskov principle.
That’s why we weakened them. If we
setWidth
accept the method as a postcondition (($this->width == $width) && ($this->height == $oldHeight))
( $oldHeight
we assigned the setWidth method at the beginning), then this condition is not satisfied in the child class, and accordingly we weakened it andпринцип Лисков
broken. Therefore, it is better not to make the “square” hierarchy inherit the “rectangle” within the framework of OOP and the task of calculating the area of a figure, but to make them as 2 separate entities:
class Rectangle
{
protected $width;
protected $height;
public setWidth($width)
{
$this->width = $width;
}
public setHeight($height)
{
$this->height = $height;
}
public function getWidth()
{
return $this->width;
}
public function getHeight()
{
return $this->height;
}
}
class Square
{
protected $size;
public setSize($size)
{
$this->size = $size;
}
public function getSize()
{
return $this->size;
}
}
A good real example of non-observance of the Liskow principle and the decision taken in this regard is considered in Robert Martin's book “Rapid Program Development” in the section “Liskow Substitution Principle. A real example. "
Interface segregation
This principle states that “Many specialized interfaces are better than one universal”.
Observance of this principle is necessary so that client classes using / implementing the interface know only about the methods that they use, which leads to a reduction in the amount of unused code.
Let us return for example with the online store.
Suppose our products may have a promotional code, a discount, they have some kind of price, condition, etc. If this is clothing, then it is set for what material it is made of, color and size.
We describe the following interface
interface IItem
{
public function applyDiscount($discount);
public function applyPromocode($promocode);
public function setColor($color);
public function setSize($size);
public function setCondition($condition);
public function setPrice($price);
}
This interface is bad in that it includes too many methods. But what if our class of goods cannot have discounts or promotional codes, or it makes no sense for it to establish the material from which it is made (for example, for books). Thus, in order not to implement unused methods in each class, it is better to split the interface into several smaller ones and implement the required interfaces with each specific class.
Splitting the IItem interface into several
interface IItem
{
public function setCondition($condition);
public function setPrice($price);
}
interface IClothes
{
public function setColor($color);
public function setSize($size);
public function setMaterial($material);
}
interface IDiscountable
{
public function applyDiscount($discount);
public function applyPromocode($promocode);
}
class Book implemets IItem, IDiscountable
{
public function setCondition($condition){/*...*/}
public function setPrice($price){/*...*/}
public function applyDiscount($discount){/*...*/}
public function applyPromocode($promocode){/*...*/}
}
class KidsClothes implemets IItem, IClothes
{
public function setCondition($condition){/*...*/}
public function setPrice($price){/*...*/}
public function setColor($color){/*...*/}
public function setSize($size){/*...*/}
public function setMaterial($material){/*...*/}
}
Dependency Invertion Principle
The principle reads: “Dependencies within the system are built on the basis of abstractions. Top-level modules are independent of lower-level modules. Abstractions should not depend on the details. Details must depend on abstractions . ” This definition can be shortened - "dependencies should be built with respect to abstractions, not details . "
For example, consider the payment of an order by a buyer.
class Customer
{
private $currentOrder = null;
public function buyItems()
{
if(is_null($this->currentOrder)){
return false;
}
$processor = new OrderProcessor();
return $processor->checkout($this->currentOrder);
}
public function addItem($item){
if(is_null($this->currentOrder)){
$this->currentOrder = new Order();
}
return $this->currentOrder->addItem($item);
}
public function deleteItem($item){
if(is_null($this->currentOrder)){
return false;
}
return $this->currentOrder ->deleteItem($item);
}
}
class OrderProcessor
{
public function checkout($order){/*...*/}
}
Everything seems quite logical and logical. But there is one problem - the class
Customer
depends on the class OrderProcessor
(moreover, the principle of openness / closure is not fulfilled). In order to get rid of dependence on a particular class, it is necessary to make it so
Customer
dependent on abstraction, i.e. from the interface IOrderProcessor
. This dependency can be injected through setters, method parameters, or the Dependency Injection
container. I decided to focus on method 2 and got the following code.Inverting Customer Class Dependencies
class Customer
{
private $currentOrder = null;
public function buyItems(IOrderProcessor $processor)
{
if(is_null($this->currentOrder)){
return false;
}
return $processor->checkout($this->currentOrder);
}
public function addItem($item){
if(is_null($this->currentOrder)){
$this->currentOrder = new Order();
}
return $this->currentOrder->addItem($item);
}
public function deleteItem($item){
if(is_null($this->currentOrder)){
return false;
}
return $this->currentOrder ->deleteItem($item);
}
}
interface IOrderProcessor
{
public function checkout($order);
}
class OrderProcessor implements IOrderProcessor
{
public function checkout($order){/*...*/}
}
Thus, the class
Customer
now depends only on the abstraction, and the concrete implementation, i.e. the details are not so important to him.Crib
Summarizing all of the above, I would like to make the following cheat sheet
- The principle of single responsibility (Single responsibility)
“Each object must be assigned one single responsibility”.
To do this, we check how many reasons we have to change the class - if more than one, then this class should be divided. - Open-closed principle
“Software entities must be open for expansion, but closed for modification”
To do this, we present our class as a “black box” and see if we can change its behavior in this case. - The principle of substitution Barbara Liskov (Liskov substitution)
"Objects in the program can be replaced by their heirs without changing the properties of the program"
To do this, we check whether we have strengthened the preconditions and weakened the postconditions. If this happens, then the principle is not respected. - The principle of interface separation (Interface segregation)
“Many specialized interfaces are better than one universal”. We
check how many interfaces contain methods and how different functions are superimposed on these methods, and if necessary, we break up the interfaces. - Dependency Invertion principle
“Dependencies should be built with respect to abstractions, not details” We
check whether classes depend on some other classes (directly instantiate objects of other classes, etc.) and if this dependency takes place, replace it with a dependency from abstraction.
I hope my cheat sheet will help someone in understanding the principles of SOLID and give impetus to their use in their projects.
Thanks for attention.
PS In the comments advised a good book - Robert Martin "Rapid software development." There, SOLID principles are described in great detail and with examples.