Начнем с того, что написание статьи про SOLID меня сподвигло одно собеседование, на которое я пришел абсолютно не готовясь и конечно же вопросы были именно по нему :). Ох как же сложно вспоминать то, что давно забыто. Я постараюсь рассказать про SOLID не просто сухими определениями которых просто куча в интернете, а показать реальные коммерческие примеры. И так погнали.
1. S - Single Responsibility Principle (Принцип единственной ответственности).
Этот принцип предписывает делить ответственность между классами: каждый из них должен отвечать только за одну определенную задачу, а не решать целый комплекс проблем. В идеале у класса должна быть одна причина для изменения.
Зачем всё это? Почему нельзя поместить весь код в один класс?
Здесь всё довольно просто:
- Во-первых , мы раздуем класс, из-за чего с ним станет банально сложнее работать.
- Во-вторых , увеличится риск конфликтов в системах контроля версий. Представьте, если бы весь код проекта лежал в одном файле. Как думаете, удобно ли было бы работать с ним команде из 3–5 разработчиков?
- В-третьих , если понадобится переиспользовать часть кода в совершенно другом модуле, придется либо копировать его, либо плодить «костыли», что приведет к лишней связанности компонентов.
Рассмотрим пример. Допустим, у нас есть интернет-магазин. Каждый день мы регистрируем заказы и отправляем клиентам уведомления об изменении их статуса. Создать метод отправки уведомления прямо в классе заказа — плохое решение. Это перегрузит класс лишним функционалом (например, настройкой работы с SMTP-сервером или SMS-службой). Кроме того, если мы решим сменить сервис отправки, код придется переписывать во всех классах, где применялся такой подход.
Оптимальное решение — создать отдельный класс-сервис для отправки уведомлений (например, EmailNotificationService ), который будет отвечать исключительно за доставку писем на почту.
2. O - Open/Closed Principle (Принцип открытости\закрытости)
Принцип основывается на расширении функционала и полиморфизме. Простыми словами мы должны легко добавлять новый функционал не делая изменения в старом. Например у нас есть интернет магазин, нам нужно сделать расчет стоимости доставки, правильным решением было бы сделать интерфейс DeliveryStrategy с методом calculate и каждую доставку сделать отдельным классом, реализующим интерфейс DeliveryStrategy с реализацией метода calculate. При таком подходе, если нам потребуется внедрить новую доставку, нам не нужно затрагивать класс DeliveryCostCalculator, достаточно просто добавить новый класс с доставкой.
// Интерфейс для доставок
interface DeliveryStrategy {
public function calculate(Order $order): float;
}
// Быстрая доставка
class ExpressDelivery implements DeliveryStrategy {
public function calculate(Order $order): float
{
return 500.00;
}
}
// Пункты выдачи
class PickupDelivery implements DeliveryStrategy {
public function calculate(Order $order): float
{
return 0.0;
}
}
// Класс расчета стоимости доставки, зависитот интерфейса
class DeliveryCostCalculator {
public function calculate(Order $order, DeliveryStrategy $strategy): float
{
return $strategy->calculate($order);
}
}
3. L - Liskov Substitution Principle (Принцип подстановки Барбары Лисков)
Принцип основывается на том, что можно взять любой дочерний класс и подставить его туда где может ожидаться родительский и ничего при этом не сломается. Теперь представим у нас появляется новый способ доставки, который возможен только если суммарный вес заказа меньше 10 кг, если мы опишем класс вот так:
class CourierDelivery implements DeliveryStrategy {
public function calculate(Order $order): float
{
if ($order->weight > 10000) {
throw new RuntimeException('Вес заказа слишком большой!');
}
return 300.0;
}
}
при расчете у нас могут возникать ошибки в DeliveryCostCalculator, правильным решением следуя данному принципу нам нужно сделать, еще один класс для проверки возможности доставки и уже непосредственно перед вызовом calculate делать проверку на возможность доставки.
class DeliveryAvailabilityChecker {
public function isAvailable(DeliveryStrategy $strategy, Order $order): bool
{
if ($strategy instanceof CourierDelivery) {
return $order->getWeight() <= 10000;
}
return true;
}
}
4. I - Interface Segregation Principle (Принцип разделения интерфейса)
Принцип который предписывает разделять большие интерфейсы на маленькие, что бы избежать зависимости классов от методов которые они не будут использовать, что в следствии может нарушать принцип подстановки.
Возьмем пример из прошлого принципа, у нас есть общий интерфейс для стратегий доставки, и нам необходимо для доставок в пункты выдачи сделать метод который будет возвращать кол-во дней хранения в ПВЗ, если мы это сделаем в нашем главном интерфейсе DeliveryStrategy тогда нам придется ставить заглушки для курьерской доставки и express, что по себе нарушает принцип подстановки.
Правильным решением тогда будет сделать отдельный интерфейс для служб доставки в ПВЗ.
interface PickpointInterface {
public function getStorageDay(): int;
}
// Пункты выдачи
class PickupDelivery implements DeliveryStrategy, PickpointInterface {
public function calculate(Order $order): float
{
return 0.0;
}
public function getStorageDay(): int
{
return 14;
}
}
5. D - Dependency Inversion Principle (Принцип инверсии зависимостей)
Это наверное самый сложный для понимания принцип, построенный на инверсии зависимостей, утверждает что высокоуровневые модули не должны зависеть от низкоуровневых модулей. Оба типа модулей должны зависеть от абстракций, а не от конкретных реализаций. Простыми словами в ваши модули должны внедрятся интерфейсы, а не конкретные их реализации.
Рассматривая предыдущие принципы, мы уже коснулись данной части, если посмотреть на класс DeliveryCostCalculator его метод calculate принимает интерфейс, т.е. у нас внедрена зависимость не с конкретной реализацией, а с интерфейсом, и мы можем передавать в него различные реализации. По факту это запрет на создание конкретного класса внутри высокоуровнего модуля.
Заключение
Рассмотрев все 5 принципов можно с уверенностью сказать, что все они взаимосвязаны, следование одному принципу подталкивает на следованию другому. Но надо помнить, что всегда есть исключения, порой ввернуть костылечек намного проще, быстрее, чем переписывать большой участок легаси кода, что опять же подтверждает важность данных принципов, проектировать хорошо надо с самого начала...
0 Сообщений