📜 ⬆️ ⬇️

Cheat Sheet on SOLID Principles with PHP Examples

The theme of the SOLID principles and the overall cleanliness of the code more than once rose on Habré and, perhaps, already quite a bit gone. But nevertheless, not so long ago I had to have interviews at one interesting IT company, where I was asked to talk about the principles of SOLID with examples and situations when I did not follow 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 name them all, but to give concise and understandable examples became a problem for me. That's why I decided for myself and for the community to compile information on SOLID principles for an even better understanding of it. The article should be useful for people who are only acquainted with SOLID-principles, as well as for people who "have eaten 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 refer directly to the cheat sheet at the end of the article.

What are SOLID principles? If you believe the definition of Wikipedia, this is:
the abbreviation of the five basic principles of class design in object-oriented design - S ingle responsibility, O pen-closed, L iskov substitution, Intuitive segregation, and D ependency inversion.

')
Thus, we have 5 principles, which we will consider below:


Single responsibility principle


So, as an example, take a fairly popular and widely used example - an online store with orders, products and customers.

The principle of sole responsibility says: “Each object should be assigned one sole responsibility . Those. in other words - a specific class must solve a specific task - no more, no less.

Consider the following class description for submitting 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 methods of printing or the work of the storage, we change the order class itself, which may lead to its inoperability.
To solve this problem is to divide this class into 3 separate classes, each of which will be engaged in its task.

 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 specific task and for each class there is only 1 reason for changing it.

The principle of openness / closeness (Open-closed)


This principle says - " , " . In simpler words, it 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 OrderRepository class.
 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 we have is a database. for example, MySQL. But suddenly we wanted to load our data about orders, for example, through the API of a third-party server, which, let's say, takes data from 1C. What changes will we need to make? There are several options, for example, directly changing the methods of the OrderRepository class, but this does not correspond to the principle of openness / closeness , 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 OrderRepository class and override all the methods, but this solution is not the best, since when adding a method to the OrderRepository we will have to add similar methods to all of its heirs. Therefore, to implement the principle of openness / closeness, it is better to apply the following solution - to create an interface IOrderSource , which will be implemented by the corresponding classes MySQLOrderSource , ApiOrderSource and so on.

Interface IOrderSource 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 OrderRepository class by setting the class we need to implement the IOrderSource , without changing the OrderRepository class.

Barbara Liskov substitution principle (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 a class heir, the result of executing the code should be predictable and not change the properties of the method.
Unfortunately, I could not think of an accessible example for this principle as part of the task of an online store, but there is a classic example with a hierarchy of geometric shapes and area calculation. Example code below.

An example of the hierarchy of a rectangle and a square and the calculation of 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.
But what's the problem? Isn't "square" a "rectangle"? Is, but in geometrical terms. In terms of objects, a square is not a rectangle, since the behavior of an object “square” is not consistent with the behavior of an object “rectangle”.

Then how to solve the problem?
The solution is closely related to the concept of contract design . Description of the design under the contract may take more than one article, so we limit ourselves to the features that relate to the Liskov principle .
Contract engineering leads to some restrictions on how contracts can interact with inheritance, namely:


“What are the pre- and post-conditions?” You may ask.
Answer : preconditions are what should be performed by the caller before calling the method, postconditions are what is guaranteed by the called method.

Let us return to our example and see how we changed the pre and post conditions.
We did not use the preconditions when calling methods for setting the height and width, but we changed the postconditions in the heir class and changed them to weaker ones, which, according to the Liskov principle, could not be done.
We weakened them, that's why. If the postcondition of the setWidth method setWidth taken (($this->width == $width) && ($this->height == $oldHeight)) (we assigned $oldHeight at the beginning of the setWidth method), then this condition is not satisfied in the child class and accordingly, we weakened it and violated.

Therefore, it is better in the framework of the PLO and the task of calculating the area of ​​a figure not to make the hierarchy “square” inherit the “rectangle”, 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 Liskou principle and the decision taken in connection with this is discussed in Robert Martin’s book “Rapid Program Development” in the section “Liskou Substitution Principle. A real example. ”

Interface segregation principle


This principle states that "Many specialized interfaces are better than one universal"
Compliance with this principle is necessary so that the client classes using / implementing the interface know only about the methods they use, which leads to a decrease in the amount of unused code.

Let's return to the example of an online store.
Suppose our products may have a promotional code, a discount, they have some kind of price, condition, etc. If it is clothing, then for it it is made of what material, 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 because it includes too many methods. And what if our class of goods can not have discounts or promotional codes, or for it does not make sense to set the material from which it is made (for example, for books). Thus, in order not to implement methods that are not used in each class, it is better to split the interface into several small ones and implement the necessary interfaces with each class.

Split 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 Inversion Principle


The principle says - “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 reduced - “dependencies should be built with respect to abstractions, not details .

For example, consider the payment of the order by the 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 Customer class depends on the OrderProcessor class (moreover, the principle of openness / closeness is not fulfilled).
In order to get rid of dependence on a particular class, you need to make Customer depend on abstraction, i.e. from the IOrderProcessor interface. This dependency can be implemented via setters, method parameters, or Dependency Injection container. I decided to stop at 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 Customer class now depends only on abstraction, and the concrete implementation, i.e. details, it is not so important.

Crib


Summarizing all 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, a good book was advised - Robert Martin "Rapid Software Development." There, the principles of SOLID are described in great detail and with examples.

Source: https://habr.com/ru/post/208442/


All Articles