SOLID principles
SOLID — 5 правил разработки ПО задают траекторию, по которой нужно следовать, когда пишешь программы, чтобы их проще было масштабировать и поддерживать.
SOLID подробнее: https://solidbook.vercel.app/
S – Single Responsibility Principle (SRP)
Не должно быть более одной причины для изменения кода класса. Другими словами, класс должен отвечать только за что-то одно.
Т.е. если нужно изменить несколько несвязанных вещей и для этого нужно менять код одного класса, то этот класс нарушает SRP.
Если класс отвечает за несколько операций сразу, вероятность возникновения багов возрастает – внося изменения, касающиеся одной из операций вы, сами того не подозревая, можете затронуть и другие.
Назначение
Служит для разделения поведения разных классов, так чтобы разное поведение не пересекалось.
Пример кода
Давайте рассмотрим пример с классом User, который НАРУШАЕТ принцип SRP:
class User { public $username; public $email; public function __construct($username, $email) { $this->username = $username; $this->email = $email; } public function save() { // Логика сохранения пользователя в базе данных } public function sendEmail($message) { // Логика отправки электронной почты пользователю } public function generateReport() { // Логика генерации отчета пользователя } }
Тут класс User отвечает за сохранение пользователя в базе данных, отправку электронной почты и генерацию отчета.
Давайте применим SRP принцип и разделим класс User на несколько отдельных классов с единой ответственностью:
class User { public $username; public $email; public function __construct($username, $email) { $this->username = $username; $this->email = $email; } public function save() { // Логика сохранения пользователя в базе данных } } class EmailSender { public function send_email($user, $message) { // Логика отправки электронной почты пользователю } } class ReportGenerator { public function generate_report($user) { // Логика генерации отчета пользователя } }
Видео
O — Open-Closed Principle (OCP)
Классы должны быть открыты для расширения, но закрыты для модификации.
Когда вы меняете текущее поведение класса, эти изменения сказываются на всех системах, работающих с данным классом. Если хотите, чтобы класс выполнял больше операций, то идеальный вариант – не заменять старые на новые, а добавлять новые к уже существующим.
Назначение
Cлужит для того, чтобы делать поведение класса более разнообразным, не трогая операции, которые он уже выполняет.
Пример кода
Правильная реализация OCP на PHP:
interface Shape { public function area(): float; } class Rectangle implements Shape { private $width; private $height; public function __construct($width, $height) { $this->width = $width; $this->height = $height; } public function area(): float { return $this->width * $this->height; } } class Circle implements Shape { private $radius; public function __construct($radius) { $this->radius = $radius; } public function area(): float { return pi() * pow($this->radius, 2); } }
Здесь мы создаем интерфейс Shape
, который определяет контракт для вычисления площади различных фигур. Классы Rectangle
и Circle
реализуют этот интерфейс и предоставляют свои специфические реализации метода area()
.
Придерживаясь принципа OCP, мы можем вводить новые фигуры без модификации существующего кода. Добавим треугольник:
class Triangle implements Shape { private $base; private $height; public function __construct($base, $height) { $this->base = $base; $this->height = $height; } public function area(): float { return 0.5 * $this->base * $this->height; } }
Тут мы добавляем новый класс Triangle
, который реализует интерфейс Shape
со своей реализацией метода area()
. Нам не потребовалось модифицировать существующие классы Rectangle
или Circle
для размещения новой фигуры.
Создавая классы и интерфейсы, открытые для расширения (добавления новых фигур) и закрытые для модификации (избегая изменения существующей логики вычисления фигур), мы следуем принципу OCP.
Помимо интерфейсов и наследования, для реализации принципа OCP могут использоваться и другие приемы, такие как:
- композиция.
- паттерн стратегия.
- инъекция зависимостей.
Видео
TODO: video_here
L — Liskov Substitution Principle (LSP)
Объекты (классы) можно заменить на под-объекты (дочерние классы) при этом программа не должна сломаться.
Если объект "B" был создан из объекта "A", то любые объекты "A" в программе, должны заменяться объектами "B" без негативных последствий для функциональности программы.
В случаях, когда потомок не способен выполнять те же действия, что и родитель, возникает риск появления ошибок.
Если вы создаете новый класс на базе другого, исходный класс становится родителем, а новый – потомком. Необходимо, чтобы потомок умел делать все точно так же, как и родитель. Например, при перегрузке метода, новый метод должен уметь все тоже самое что и исходный метод (получать такие же параметры, возвращать такие же данные/типы) и только потом делать что-то еще.
Если потомок не удовлетворяет этим требованиям, значит, он слишком сильно отличается от родителя и нарушает принцип LSP.
Назначение
Служит для обеспечения постоянства: потомок может быть использован вместо родителя без нарушения работы программы.
Видео
I — Interface Segregation Principle (ISP)
Клиент должен знать о методах класса, которые нужны ему и не знать ничего о каких-либо других методах.
Много мелких классов, которые подходят каждому в отдельности лучше чем один, который подходит всем сразу.
Одному клиенту нужен один функционал, другому другой, но оба функционала похожи. Лучше создать два класса (итерфейса) и дать каждому клиенту свой. Принцип нарушается если у нас есть один класс, который подходить разным клиентам.
Назначение
Служит для того, чтобы раздробить единый набор действий на ряд наборов поменьше. Таким образом, каждый класс делает то, что от него действительно требуется, и ничего больше.
Пример кода
Давайте рассмотрим пример на Python с интерфейсами для различных устройств вывода ввода:
abstract class InputDevice { abstract public function readInput(); } abstract class OutputDevice { abstract public function writeOutput($data); } class Keyboard extends InputDevice { public function readInput() { // Логика чтения ввода с клавиатуры } } class Mouse extends InputDevice { public function readInput() { // Логика чтения ввода с мыши } } class Monitor extends OutputDevice { public function writeOutput($data) { // Логика вывода данных на монитор } } class Printer extends OutputDevice { public function writeOutput($data) { // Логика вывода данных на принтер } }
В этом примере у нас есть абстрактные классы InputDevice
и OutputDevice
, представляющие интерфейсы для устройств ввода и вывода соответственно. Затем мы определяем реализации этих абстракций в виде классов: Keyboard
, Mouse
, Monitor
и Printer
.
Клиенты будут зависить от интерфейсов, а не от реализаций. Например, если клиенту нужен только ввод с клавиатуры, он может зависеть только от интерфейса InputDevice
и использовать класс Keyboard
:
def process_input(device): data = device.read_input() # Логика обработки ввода keyboard = Keyboard() process_input(keyboard)
В этом примере функция process_input()
принимает объект, реализующий интерфейс InputDevice
, и обрабатывает его ввод. Здесь мы передаем объект Keyboard
, который соответствует интерфейсу InputDevice
. Таким образом, клиент зависит только от необходимого интерфейса и не зависит от лишних методов или классов.
Видео
TODO: video here
D — Dependency Inversion Principle (DIP)
Модули верхнего уровня не должны зависеть от модулей нижнего уровня. И те, и другие должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Абстракция
= интерфейс, соединяющий два класса.Модули (классы) верхнего уровня
= классы, которые выполняют операцию при помощи инструмента.Модули (классы) нижнего уровня
= инструменты, которые нужны для выполнения операций.Детали
= специфический код внутри инструмента - реализация конкретной задачи в коде.
Класс не должен напрямую соединяться с конкретным инструментом, который выполняет операцию. Вместо этого он должен быть соединён с интерфейсом (контроллером, абстракцией), который устанавливает связь между классом (модулем верхнего уровня) и инструментом (модулем нижнего уровня).
Ни интерфейс, ни класс, не должны вникать в специфику работы инструмента (не должны использовать его методы, переменные, константы и т.д). Напротив, это инструмент должен подходить под требования интерфейса.
Назначение
Служит, чтобы устранить зависимость классов верхнего уровня от классов нижнего уровня за счёт введения прослойки (интерфейса, абстрации).
Так при расширении функционала, нам не нужно будет менять верхний уровень, а нужно будет написать новый функционал и прикрутить его к верхнему уровную, через абстракцию. Т.е. все новое будет писаться отдельно и добавляться в абстракцию. А базовый (верхний) код изменяться вообще не будет.
Пример кода
Давайте рассмотрим пример на python с классами Notification
и EmailSender
. Данный код нарушает принцип DIP:
interface Notification { public function sendNotification( $message ); } class EmailSender implements Notification { public function sendNotification( $message ) { // send notification via email } } class SMSNotification implements Notification { public function sendNotification( $message ) { // send notification via SMS } } class User { private $username; private $email; private $notificationService; public function __construct( $username, $email ) { $this->username = $username; $this->email = $email; // Change this to use desired notification service $this->notificationService = new EmailSender(); } public function sendNotification($message) { $this->notificationService->sendNotification($message); } }
В этом примере класс User
напрямую зависит от конкретной реализации EmailSender
в качестве сервиса уведомлений.
Чтобы применить SOLID принцип DIP, изменяем User
, чтобы он зависел от абстракции Notification
, а не от конкретной реализации:
class User { private $username; private $email; private $notificationService; public function __construct( $username, $email, $notificationService ) { $this->username = $username; $this->email = $email; $this->notificationService = $notificationService; } public function sendNotification( $message ) { $this->notificationService->sendNotification( $message ); } }
Теперь User
принимает объект notification_service
, реализующий интерфейс Notification
. Это позволяет передавать различные реализации уведомлений, например EmailSender
или SMSNotification
, без изменения самого User:
$emailSender = new EmailSender(); $user1 = new User( "John", "john@example.com", $emailSender ); $user1->sendNotification( "Hello!" ); $smsNotification = new SMSNotification(); $user2 = new User( "Jane", "jane@example.com", $smsNotification ); $user2->sendNotification( "Hi there!" );
Теперь User зависит от абстракции Notification
и может быть легко настроен для работы с различными реализациями уведомлений. Это уменьшает связанность между классами, делает их более гибкими и легкими для тестирования и модификации.
Видео
--
--
Фото были взяты здесь: https://habr.com/ru/company/productivity_inside/blog/505430/