Бортовой журнал Ктулху

Объяснение принципов SOLID в примерах

100500-я статья про солид.

Об этом можно писать бесконечно и все равно ничего не ясно.

--

SOLID - это аббревиатура, которая объединяет в себе 5 принципов, способствующих написанию правильного кода (и каждый уважающий себя HR напишет эту аббревиатуру в описание вакансии, хотя не понимает что оно такое).

1). Single Responsibility Principle (Принцип единственной ответственности) - это означает, что каждый класс должен отвечать только за одну задачу. Например, если у вас есть класс "Корзина", он должен отвечать только за добавление и удаление товаров из корзины, но не должен отвечать за оплату или оформление заказа.


Пример принципа единственной ответственности можно привести на основе класса "Пользователь" в системе авторизации. Если мы будем следовать принципу единственной ответственности, то класс "Пользователь" должен заниматься только управлением пользователями, а не каким-то другим функционалом.

Вот пример реализации этого принципа:

class User {
public function create($username, $password) {
// записать в БД
}
public function delete($userId) {
// удалить из БД
}
public function update($userId, $username, $password) {
// обновить инфо
}
}

В этом примере класс "User" имеет только три метода, которые отвечают за создание, удаление и обновление пользователя в базе данных. Класс не отвечает за другие задачи, такие как проверка пароля или отправка электронной почты, и это позволяет легко понимать и использовать его код.

Если бы мы добавили в класс "User" методы для проверки пароля или отправки электронной почты, это бы нарушило принцип единственной ответственности и сделало класс более сложным для понимания и использования.

 

2). Open-Closed Principle (Принцип открытости/закрытости) - это означает, что классы должны быть открыты для расширения, но закрыты для изменения. Например, если у вас есть класс "Фигура", вы можете расширить его, добавив новую фигуру, но вы не должны изменять существующий код, чтобы поддерживать новую фигуру.


Принцип открытости/закрытости можно проиллюстрировать на примере класса "Фигура", который имеет методы для вычисления площади и периметра фигуры. Если мы будем следовать принципу открытости/закрытости, то класс "Фигура" должен быть открыт для расширения новыми фигурами, но закрыт для изменения существующего кода.

Вот пример реализации этого принципа:

// абстракция, не указано ж какая именно фигура
abstract class Shape {
abstract public function getArea();
abstract public function getPerimeter();
}
 
// а вот тут уже вполне определенная фигура, которая может иметь свойства, отличные от базовых
class Rectangle extends Shape {
private $width;
private $height;
public function __construct($width, $height) {
$this->width = $width;
$this->height = $height;
}
public function getArea() {
return $this->width * $this->height;
}
public function getPerimeter() {
return 2 * ($this->width + $this->height);
}
}
 
// свойства круга могут отличаться от свойств прямоугольника, но не тащить же весь набор свойств в базовый класс 
class Circle extends Shape {
    private $radius;
    public function __construct($radius) {
        $this->radius = $radius;
    }
    public function getArea() {
        return pi() * pow($this->radius, 2);
    }
 
    public function getPerimeter() {
        return 2 * pi() * $this->radius;
    }

}

 

В этом примере абстрактный класс "Shape" (фигура) определяет методы для вычисления площади и периметра фигуры. Класс "Rectangle" и "Circle" наследуются от абстрактного класса "Shape" и реализуют свои собственные методы для вычисления площади и периметра.

Если нам потребуется добавить новую фигуру, например, "Треугольник", мы можем создать новый класс "Triangle", который также будет наследоваться от абстрактного класса "Shape" и реализовывать свои собственные методы для вычисления площади и периметра, не изменяя при этом код класса "Shape".

Таким образом, мы можем расширять функциональность нашей системы без изменения уже существующего кода.

 

3). Liskov Substitution Principle (Принцип подстановки Лисков) - это означает, что вы должны использовать наследование только тогда, когда вы можете заменить экземпляр базового класса экземпляром производного класса без нарушения работы программы. Например, если у вас есть класс "Животное" и класс "Собака", то вы можете использовать экземпляры класса "Собака" везде, где ожидается экземпляр класса "Животное".

Принцип подстановки Лисков (Liskov Substitution Principle, LSP) является одним из основных принципов SOLID в объектно-ориентированном программировании. Он утверждает, что подклассы должны быть взаимозаменяемы со своими базовыми классами, т.е. код, написанный для базового класса, должен работать без изменений с объектами подкласса.

Вот пример кода на PHP, демонстрирующий принцип подстановки Лисков:


class Shape {
protected $width;
protected $height;
 
public function setWidth($width) {
    $this->width = $width;
 }
 
public function setHeight($height) {
    $this->height = $height;
 }
 
public function getArea() {
    return $this->width * $this->height;
 }
}
 
class Rectangle extends Shape {
    // Класс для прямоугольника
}
 
class Square extends Shape {
    // Класс для квадрата
    public function setWidth($width) {
        $this->width = $width;
        $this->height = $width;
    }
    public function setHeight($height) {
        $this->height = $height;
        $this->width = $height;
    }
}
 
function printArea(Shape $shape) {
    $shape->setWidth(5);
    $shape->setHeight(10);
    echo 'Area: ' . $shape->getArea() . '<br>';
}
 
$rectangle = new Rectangle();
printArea($rectangle);
$square = new Square();
printArea($square);

В этом примере классы Rectangle и Square наследуют класс Shape. Метод printArea принимает объект типа Shape в качестве параметра и выводит его площадь. Объекты класса Square переопределяют методы setWidth и setHeight таким образом, чтобы устанавливать и ширину, и высоту равными переданному значению (это свойственно только для квадрата).

Принцип подстановки Лисков выполняется, потому что объекты Rectangle и Square могут быть использованы вместо объекта Shape без изменения поведения метода printArea.

 

4). Interface Segregation Principle (Принцип разделения интерфейсов) - это означает, что вы должны создавать интерфейсы, которые содержат только необходимые методы. Например, если у вас есть класс "Работник", который имеет методы "Работать" и "Отдыхать", вы можете разделить интерфейс на "Работающий" и "Отдыхающий", чтобы классы могли реализовывать только нужные методы.


Принцип разделения интерфейсов (Interface Segregation Principle, ISP) является одним из принципов SOLID-подхода к проектированию программного обеспечения. Он утверждает, что клиенты не должны зависеть от методов, которые они не используют.

В PHP пример использования ISP может выглядеть следующим образом:

interface Car {
public function start();
public function stop();
}
 
interface Radio {
public function turnOn();
public function turnOff();
public function setVolume($volume);
}
 
class LuxuryCar implements Car, Radio {
public function start() {
// код для запуска машины
}
 
public function stop() {
// код для остановки машины
}
 
public function turnOn() {
// код для включения радио
}
 
public function turnOff() {
// код для выключения радио
}
 
public function setVolume($volume) {
// код для установки громкости радио
}
}
 
class EconomyCar implements Car {
public function start() {
// код для запуска машины
}
 
public function stop() {
// код для остановки машины
}
}

В данном примере мы имеем два интерфейса: Car и Radio. Интерфейс Car определяет два метода: start() и stop(), которые необходимы для любой машины. Интерфейс Radio определяет три метода: turnOn(), turnOff() и setVolume(), которые могут быть не нужны для всех типов машин.

Класс LuxuryCar реализует оба интерфейса, так как люксовая машина должна иметь как радио, так и двигатель. Класс EconomyCar реализует только интерфейс Car, так как экономичная машина не обязательно должна иметь радио.

Таким образом, использование принципа разделения интерфейсов позволяет создавать более гибкие и расширяемые классы, так как они не зависят от методов, которые они не используют. Это упрощает поддержку кода и повышает его качество.

 

5). Dependency Inversion Principle (Принцип инверсии зависимостей) - это означает, что классы верхнего уровня не должны зависеть от классов нижнего уровня. Например, если у вас есть класс "Сайт", который зависит от класса "База Данных", вы можете создать абстракцию, которая будет использоваться классом "Сайт", вместо того чтобы он зависел от класса "База Данных" напрямую.

Принцип инверсии зависимостей (Dependency Inversion Principle, DIP) является одним из принципов SOLID-подхода к проектированию программного обеспечения. Он утверждает, что модули верхнего уровня не должны зависеть от модулей нижнего уровня, а оба типа модулей должны зависеть от абстракций.

В PHP пример использования DIP может выглядеть следующим образом:

interface DatabaseInterface {
public function connect();
public function query($sql);
}
 
class MysqlDatabase implements DatabaseInterface {
public function connect() {
// код для подключения к MySQL-базе данных
}
 
public function query($sql) {
// код для выполнения запроса к MySQL-базе данных
}
}
 
class PostgresDatabase implements DatabaseInterface {
public function connect() {
// код для подключения к Postgres-базе данных
}
 
public function query($sql) {
// код для выполнения запроса к Postgres-базе данных
}
}
 
class UserManager {
private $database;
public function __construct(DatabaseInterface $database) {
$this->database = $database;
}
 
public function getUserById($id) {
$sql = "SELECT * FROM users WHERE id = $id";
$result = $this->database->query($sql);
// код для обработки результата запроса и возврата данных о пользователе
}
}

В данном примере мы имеем интерфейс DatabaseInterface, который определяет два метода: connect() и query(). Классы MysqlDatabase и PostgresDatabase реализуют этот интерфейс и предоставляют соответствующие реализации методов для подключения и выполнения запросов к соответствующим базам данных.

Класс UserManager зависит от интерфейса DatabaseInterface вместо конкретных реализаций баз данных, что позволяет легко заменять одну базу данных на другую, не изменяя код класса UserManager. Это осуществляется за счет инъекции зависимости через конструктор, где объект класса, реализующего интерфейс DatabaseInterface, передается в качестве аргумента.

Таким образом, использование принципа инверсии зависимостей позволяет создавать более гибкие и расширяемые классы, так как они не зависят от конкретных реализаций зависимостей, а только от их абстракций. Это упрощает поддержку кода и повышает его качество.