SOLID принципы часть 3
Принцип отделения интерфейсов (ISP Interface Segregation Principle)
Каноническое определение принципа:
Клиенты не должны попадать в зависимость от методов, которыми они не пользуются
Этот принцип про интерфейсы, про то как их надо создавать.
Интерфейс является абстракцией внешнего представления класса и описывает все доступные методы для работы с ним. В языках, в которых нет поддержки интерфейсов, они эмулируются через классы с пустыми методами. То есть интерфейсы это классы у которых есть исключительно публичные методы, и у них нет реализации, они должен реализовываться классами их использующими.
Цель принципа:
- Принцип борется с толстыми интерфейсами (то есть интерфейсами на все случаи жизни)
- По сути это принцип персональной отвественности, но для интерфейсов (то есть каждый интерфейс должен делать одну свою задачу)
- Интерфейс должен быть абстрактным (иметь универсальное имя и никому не принадлежать)
Пример
Допусти у нас есть класс Product и интерфейс IProduct очевидно что не все классы Product будут поддерживать все его возможные интерфейсы
interface IProduct { function getPrice() function getColor() function getModel() function getPublishYear() function getMaxDiscount() function checkPromocode(promocode) }
В нашем пример
- Нарушен принцип персональной отвественности
- Неизвестно, как реализовать в классе не свойственные для него методы (либо будут пустыми , либо будут содержать исключения)
- Интерфейсы не должны привязываться к реализации. Имена должны быть универсальными. (Имя должно говорить то что делает интерфейс )
Для того что бы избавиться от этой проблемы, мы делим наш класс на состовлящие:
interface IBaseInfo { function getPrice() function getColor() } interface ICharacteristic { function getModel() function getPublishYear() } interface IDiscount { function getMaxDiscount() funciton checkPromocode(promocode) }
Принцип применяется
- Для создания абстракций (повышение гибкости)
- Так же используется в принципе DIP
Опасности
Добавление метода в интерфейс заставляет реализовывать его в классах наследниках
Заранее хорошо продумывайте абстракции. То есть заранее продумывайте интерфейс а потом класс. Если происходит наоборот то есть с начало реализовывается класс а потом часть его методов переходит в интерфейс, то стоит задуматься а все ли я делаю правильно. Это пагубная привычка и признак возможной проблемы.
Суть принципа
Много специализированных интерфейсов лучше, чем один универсальный.
Принцип инверсии зависимостей (DIP Dependency Inversion Principle)
Каноническое определение принципа:
Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба типа модулей обязаны зависеть от абстракций. Абстракции не должны зависеть от подробностей. Подробностям следует зависеть от абстракций
Достаточно большое определение, и возможно тяжелое для понимания когда читаешь его в первый. Поэтому давайте посмотрим на картинки:
На этой картинке отображена иерархия модулей. High Level Class это первичные алгоритмы высокого уровня которые могут вызвать Low level class, то есть алгоритмы более низкого уровня. Так вот на этой картинки отображено что алгоритмы высокого уровня не должны напрямую зависеть от модулей низкого уровня. Они должны быть связаны через абстракции, в данном случае через интерфейсы.
Далее для того что бы понять, что такое инверсия зависимостей давайте разберем, что такое прямая зависимость. На картинке ниже отображения прямая зависимость компонентов.
В этом случае Component A (например компонент проверки права доступа) запускает Component B (например какой нибудь компонент бизнес логики), которые в свою очередь запускает Component C (например какой нибудь вызов ORM). В данном случае мы видим что все компоненты жестко связанны и разделить их не возможно. Поэтому получается что все зависит в прямом направление.
В коде эта конструкция могло бы выглядеть как то так:
class Controller { function getMethod() { businessLogic = new BusinessLogic() } } class BusinessLogic { function getProduct() { orm = new ORM() } } class ORM { }
Опишем признаки и проблемы прямой зависимость
- В классическом программирование классы инициируются через new Class()
- Подменить реализацию нижнего уровня не возможно
- Как результат уровни зависят друг от друга, логику абстракций сложно удержать в одном месте и не размывать по разным слоям
- Невозможно написать хорошие юнит тесты
Что бы решить все эти проблемы нам на помощь приходит инверсия зависимостей. Представим наш первый рисунок в виде инверсии зависимостей:
Тут видно что все наши зависимости стали инвертируемы. То есть теперь не верхние модули зависят от нижних, а нижние от верхних. На этом рисунки у каждого компонента есть свой собственный интерфейс, и этот компонент знает о нижнем классе, только то что может знать интерфейс. При этом модуль нижнего уровня не может общаться с модулем верхнего уровня напрямую, только через его интерфейс.
Признаки инверсии
- Зависимость модулей инвертирована по отношению друг к другу, т.е. нижний модуль внедряется в верхний
- Абстракция принадлежит верхнему модулю, от которой в свою очередь зависит нижний
- Есть возможность изменить нижний уровень
- Инверсия увеличивает сцепление кода и уменьшает связанность
Рассмотрим пример инверсии зависимостей в коде. Пусть у нас есть следующий пример кода с прямой зависимостью.
class Order { function getTotalAmount() { discount = new Discount() double totalPrice = discount.calculate() } } order = new Order()
Тут видно что мы в методе getTotalAmount напрямую вызываем класс Discount. Теперь перепишем эту конструкцию с использованием принципа DIP. В примере ниже мы используем абстрактные средства языка интерфейсы:
interface IDiscount { function calculate() } class Order { function getTotalAmount(Discount discount) { double totalPrice =discount.calculate() } } сlass VIPDiscount interface IDiscount { function calculate() {...} } order = new Order().getTotalAmount(new VIPDiscount())
В этом пример мы описали интерфейс IDiscount и создали специальный класс VIPDiscount использующий этот интерфейс. А потом при создание класса Order и вызове метода getTotalAmount мы переделали в него наш специальный класс VIPDiscount. Таким образом мы избавились от прямой зависимости класса Order от класса Discount.
Рассмотрим еще один пример но теперь без использования абстрактных средств языка. Далее у нам будет класс Printer который будет использовать для печати данных в разных форматах. Достаточно типичный пример. Внутри основного класса Printer вызываются два, более низший класса PdfFormatter и HtmlFormatter
class PdfFormatter function format(data) # format data to Pdf logic class HtmlFormatter function format(data) # format data to Html logic class Printer function Printer(data) this.data = data function print_pdf() PdfFormatter(this.data) function print_html() HtmlFormatter(this.data)
Теперь перепишем этот пример с использованием принципа DIP:
class PdfFormatter function format(data) # format data to Pdf logic class HtmlFormatter function format(data) # format data to Html logic class Printer function Printer(data) this.data = data function print(PdfFormatter formatter) formatter.format(this.data)
В приведенном выше коде класс Printer — объект высокого уровня — теперь не зависит напрямую от реализации объектов низкого уровня — HtmlFormatter и PdfFormatter. Кроме того, все модули зависят от абстракции. Теперь наша высокоуровневая функциональность отделена от всех низкоуровневых деталей, поэтому мы можем легко изменить низкоуровневую логику без последствий для всей системы.
Особенности принципа
- изменения реализации модулей низкого уровня не должны изменить модули высокого уровня (проблема абстракции или отсутствие инверсии)
- Языки программирования предостовляют свои классы высокого уровня такие как String, Date, Time, и т.п. И их использование не создает ни какой проблемы зависимостей. Потому что эти зависимости статичны, в них ничего не меняется.
- В идеале интерфейс должен быть общим и не от кого ни зависить
- Всегда помните, изменяя интерфейс абстракции, меняется реализация нижнего слоя
Данный принцип позволяет создать слоистость приложения
Более высокие модули реализуют бизнес модель приложения.
Низкие модули реализуют конкретные действия (запросы в БД, сложные вычисления и т.д.) Их удобно переиспользовать в других программах в виде библиотек и общих модулей.
Суть принципа
Зависимости должны строиться относительно абстракций, а не деталей.
Заключение
В заключение, помните, что принципы SOLID сами по себе не гарантируют отличную объектно-ориентированную архитектуру. Применяйте SOLID принципы разумно. Постарайтесь хорошо понять какую именно проблему вы решаете, и действительно ли эта проблема представляет собой риск для вашей системы. Например, чрезмерное разделение классов в соответствии с принципом SRP может привести к низкой согласованностью, и даже к потере производительности.
Простая установка галочек и следованию утверждению «Теперь мой код соответствует принципам разработки SOLID» — неправильный подход. Помните утверждение, что чистый код нельзя написать, просто следуя набору правил. Однако, правильное применении SOLID рекомендаций может помочь вам создать архитектуру системы, которую легко модифицировать и расширять с течением времени, и именно к этому должен стремиться каждый разработчик.