Lecture
SOLID is an abbreviation of the five basic principles of class design in object-oriented design — Single responsibility, Open-closed, Liskov substitution, Interface segregation, and Dependency inversion.
Letter | Means | Description |
---|---|---|
S | Single responsibility principle |
Principle of sole duty Each class must have one sole responsibility. |
O | Open / closed principle |
Principle of openness / closeness Software entities should be open for expansion, but closed for change. |
L | Liskov substitution principle |
Barbara Liskov substitution principle Objects in the program can be replaced by their heirs without changing the program properties. See also contract programming. |
I | Interface segregation principle |
Interface separation principle Many specialized interfaces are better than one universal. |
D | Dependency inversion principle |
Principle of dependency inversion Dependencies within the system are based on abstractions. Top level modules are independent of lower level modules. Abstractions should not depend on the details. Details must depend on abstractions. |
The abbreviation SOLID was proposed by Robert Martin. in the early 2000s [3] , which meant the five basic principles of object-oriented programming and design.
These principles, when applied together, are designed to increase the likelihood that a programmer will create a system that will be easy to maintain and expand over time [3] . The principles of SOLID are guidelines that can be applied while working on software to remove “sniffed code” by instructing a programmer to refactor the source code until it becomes legible and extensible. It is part of an overall strategy for agile and adaptive development.
Not long ago, I had to have interviews at an 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 for me became a problem. 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:
abbreviation of the five basic principles of class design in object-oriented design - Single responsibility, Open-closed, Liskov substitution, Interface segregation, and Dependency inversion.
Thus, we have 5 principles, which we will consider below:
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 states: “There must be one sole responsibility for every object . ” 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 store ( 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 can 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.
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 example of 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 order data, 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 necessary class that implements the IOrderSource , without changing the OrderRepository 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 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 within the framework 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 of 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 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 within the framework of the PLO and the task of calculating the area of a figure not to do 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 principle of Liskou and the decision taken in connection with this is discussed in Robert Martin’s book “Rapid Program Development” in the section “Principle of Liskou Substitution. A real example. ”
This principle states that "Many specialized interfaces are better than one universal"
Adherence to 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 go back 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 is made of what material is made, 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 him it makes no sense to install 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){/*...*/} }
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 class Customer depends on the class OrderProcessor (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.
Invert Customer Class Dependency
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.
Comments
To leave a comment
Object oriented programming
Terms: Object oriented programming