5 принципов SOLID — объяснение на пальцах

Принципы SOLID состоят из 5 ключевых идей по написанию и проектированию объектно-ориентированных приложений. Принцип SOLID, сама его идея появилась в 2000 году Робертом Мартином (однако, само официальное название этому принципу были утверждено только спустя несколько лет). Принципы, именуемые, как SOLID были настолько хороши, что спустя лишь небольшое время они захватили внимание сообщества программистов.
Так же, совутую, после изучения материала этой статьи (или даже во время), ознакомиться с принципами SOLID в картинках.
Сейчас, если посмотреть на какой-то из сайтов по поиску работы, можно увидеть, что в большинстве вакансий на должность PHP разработчика, стоит требование о знании принципов SOLID. То есть, знание ООП (синтаксиса построения классов, интерфейсов, наследования и т.д.) очень часто недостаточно. Сейчас требуется более глубокое понимание принципов проектирования объектно-ориентированных систем, чем просто обычное знание синтаксиса. Я веду к тому, что знание того, как объявить класс — это ещё не значит, что вы пишите объектно-ориентированный код. И эта статья, вместе с принципами SOLID призвана к повышению вашей квалификации как программиста и поможет сделать шаг навстречу к профессиональному и качественному написанию кода.
Само определение SOLID — это аббревиатура, состоящая из таких сокращений:
- S — Принцип единственной ответственности (Single Responsibility Principle)
- O — Принцип открытости/закрытости (Open-closed Principle)
- L — Принцип подстановки Барбары Лисков (Liskov Substitution Principle)
- I — Принцип разделения интерфейса (Interface Segregation Principle)
- D — Принцип инверсии зависимостей (Dependency Inversion Principle)
Принцип единственной ответственности
Этот принцип является базовым, и одним из ключевых. Мне кажется, что многие программисты при написании кода интуитивно следуют этому принципу, не задумываясь об этом, и даже не зная о его сущестовавании. Однако, если вы поймёте идею этого принципа сейчас, и начнете разработку, держа её в голове, это, вероятно избавит вас от рефакторинга кода в будущем.
Главная идея, проходит нитью по названию этого принципа, она состоит в том, что каждый класс должен выполнять только одну задачу, и только одну.
Каждый класс должен получать информацию из системы ровно столько, сколько достаточно для работы логики этого класса. А так же, класс не должен быть мастером на все руки, выполняя множество разноплановых задач. Он должен выполнять только одну конкретную задачу. Класс может содержать много методов, но все они должны быть направлены на решение одной и только одной задачи.
Посмотрите на пример плохо спреэктированного класса:
class BadlyDesignedBlogClass < public function blogPost($post_id) < if (!\Auth::check() ) < throw new \Exception("Вы не авторизованы."); >$db = $this->dbConnection(); $sql_query = "select * from posts where $post = $this->dbRawSql($db, $sql_query, $post_id); if (!empty($_POST['title'])) < $this->editPost($post, $_POST['title']); echo "Успешно сохранено!
"; return true; > echo "Пост #" . e($post->id). ": " . e($post->title) . "
"; echo "Для редактирования заполните эту форму
"; echo ""; return true; > protected function editPost(BlogPost $post, $new_title) < // обновление информации о посте >protected function dbConnection() < // подключение к БД >protected function dbRawSql($db, $sql, $params) < // выполнение запроса в БД >>
А в контроллере этот класс вызывается как-то так:
class PostsController < public function show($post_id) < $post = new BadlyDesignedBlogClass(); $post->blogPost($post_id); > >
В классе BadlyDesignedBlogClass , сходу можно увидеть основную проблему, которая связана с тем, что этот класс имеет слишком много обязанностей:
- Проверка аутентификации пользователя.
- Устанавливает подключение к базе данных, и выполняет sql запросы, работая с подключением напрямую.
- Один метод совмещает в себе 2 обязанности: по отображению поста и по его редактированию (если $POST[‘title’] заполнен)
- Отображает html-код поста и формы редактирования внутри метода.
Вместо этого, любой из классов должен иметь только по одной обязанности
Потому, сейчас перепишем этот класс с учётом новых требований.
- При работе с базой данных я предпочитаю использовать классы-репозитории, которые внутри себя подключаются к базе данных и выполняют все запросы.
- Проверка аутентификации должна производиться в контроллере, а не внутри этого класса (как вариант использования — проверку аутентификации пользователя можно вынести в middleware, или в правила конфигурации маршрутов).
- Отображение вёрстки вынесем в отдельные файлы представлений.
class BlogController < protected $blog_repository; public function __construct(BlogRepository $blog_repository) < $this->blog_repository = $blog_repository; > public function show($post_id) < if (!\Auth::check()) < throw new \Exception("Вы не зарегистрированы. Пройдите регистрацию и повторите ваш запрос."); >// $post = new BadlyDesignedBlogClass(); // $post->blogPost($post_id); $post = $this->blog_repository->find($post_id); return view('show_post.php', ['post' => $post]); > public function update($post_id) < $post = $this->blog_repository->find($post_id); $post->update(['title' => $_POST['title']); return view('updated_post.php'); > > class BlogRepository < protected $connection; public function __construct($connection) < $this->connection = $connection; > public function find($id) < // запрос к БД по поиску поста по идентификатору // $this->connection->query("SELECT * FROM `posts` WHERE $id); > >
В коде появился новый класс — BlogRepository , который в конструктор принимает класс подключения к базе данных (условно, скажем, что это объект PDO ), который позволяет выполнять к ней запросы. И теперь только класс BlogRepository отвечает за всю работу с базой данных, касаемо статей, исключая работу с запросами к БД из контроллера напрямую.
Другое изменение было применено к проверке аутентификации пользователя. Эта обязанность была вынесена в контроллер, где ему и место.
А так же, вся разметка была вынесена из класса в отдельный файл представлений, и подключается функцией view() .
Так же, и код самого контроллера был немного изменён. Теперь контроллер имеет 2 метода для 2 разных задач. Эти методы получают пост по идентификатору (с помощью BlogRepository ), а после — отображают представление, в зависимости от того, отображается этот пост, или редактируется. В отличии же, от того, что было ранее, когда один метод отправлял все данные объекту BadlyDesignedBlogClass , который сам занимался отображением контенета.
Новая версия выглядит на порядок чище, проще для тестирования, и обновления.
Из этого примера, я надеюсь, стало понятно, что класс должен иметь только одну обязанность (при изменении одной части кода в классе, эти изменения должны повлиять на весь код, где этот класс используется). К примеру, при обновлении метода find() класса BlogRepository , изменив драйвер подключения к БД, новые изменения будут применены ко всему коду, где этот класс был задействован. В отличии от того, когда запросы к базе данных писались нативно. И, при необходимость замены драйвера подключения, или синтаксиса запросов, пришлось бы править каждый файл, где этот код написан.
Принцип Открытости/Закрытости
Следующий принцип SOLID — Принцип Открытости/Закрытости , который, по мнению идейного прородителя SOLID-а, является одним из самых важных принципов, с чем, впрочем, сложно спорить.
Этот принцип гласит, что классы должны быть открыты к расширению, и закрыты к модификации. Это значит, что ваш код должен быть написан так, чтобы другие разработчики (а, возможно, и вы, в будущем) могли изменить поведение существующего кода, при этом, не изменяя основной код. То есть, ваши классы должны быть такими, чтобы каждый мог дописать что-то своё, не меняя при этом содержимого исходного кода.
Например, если вы когда-то использовали шаблонизатор Twig, то вы наверняка писали плагины и расширения для него (добавляя собственные функции или фильтры в шаблонизатор). И как раз, из-за следования этому принципу, вы, написав собственное расширение (по соответствующему интерфейсу), добавили новую функциональность без изменения кода самого шаблонизатора.
Когда мы говорим о написании класса, открытого к расширению, обязательным условием его реализации является наследование и следование интерфейсам.
Код интерфейсов не должнен изменяться (закрыты для модификации), в то время, как классам-реализациям должно быть достаточно текущего интерфейса без нужды его изменения. То есть, вы должны писать интерфейсы таким образом, чтобы в будущем любой программист мог создать свою реализацию вашего интерфейса, без нужды внесения в него изменений. Это и является ключом к написанию кода, соответствующему этому принципу.
А, чтобы более чётко понимать, как работать с интерфейсами и абстрактыми классами, советую почитать мою предыдущую статью на эту тему.
Принцип подстановки Барбары Лисков
Принцип подстановки Барбары Лисков (именуемый как LISP) это важная концепция, придуманная ещё в далёком 1987-м. Я опишу этот принцип на основу определений, условного описания кода, вместо реализации большого примера с кодом (чтобы сделать этот пример легче для понимания, я буду использова определение, как «Клиентский класс»).
Клиентский класс должен иметь возможность использовать базовый класс или любой подкласс, производный от базового, при этом не изменяя собственный код или логику работы, независимо от реализации. То есть, родительский класс должен быть полностью взаимозаменяемым любыми его подклассами, при этом, клиентский код даже не должен знать о том, что мы работаем с каким-то подклассом, ему не должно быть разницы.
По-другому: если вы имеете базовый класс, и вы имеете 5 разных классов со своей реализацией, унаследовавших этот базовый класс. И ваш код, использующий этот базовый класс, при его замене на любой из его «наследников», по-прежнему должен продолжать работать, как и ранее.
Для соблюдения этого принципа, при наследовании базового класса, в подклассах вы должны поддерживать соответствие входных параметров и выходных (иметь одинаковые типы и соответствие сигнатур методов), как это было в базовом классе.
Принцип разделения интерфейса
Принцип разделения интерфейса (именуемый как ISP) — это принцип, который гласит, что клиентский код не должен имплементировать интерфейсы, которые он не использует, не должен определять методы, которые ему не нужны. Это гласит о том, что лучше иметь много «тонких» интерфейсов, чем несколько, но «жирных», содержащих слишком много методов, которые клиентский код не будет использовать, но будет вынужден реализовывать. В PHP класс может имплементировать больше чем один интерфейс, и этим нужно пользоваться.
Принцип инверсии зависимостей
Это мой самый любимый принцип, которы помогает поддерживать бОльшую чистоту кода, который легче тестировать и изменять в будущем.
Если вы имеете несколько клиентских классов которые предполагают работу с базой данных, вы могли бы создать класс под названием MysqlDBConnection (и использовать сервис контейнер для биндинга класса). Но что будет, если вы захотите изменить ваш драйвер БД на SQLite? Вы будете делать новую реализацию класса так: class SQLiteDBConnection extends MysqlDBConnection ?
Нет, более чисто и правильно было бы создать интерфейс DBConnectionInterface , а уже на основе него, создать соответствующие реализации классов для работы с Mysql и SQLite. После чего, в вашем приложении (для примера, в сервис-контейнере) просто нужно будет выполнить биндинг реализации к классу-интерфейсу.
После чего, вы можете иметь несколько реализаций на основе этого интерфейса, легко переключаемых между собой. При этом, замена реализаций иключает изменение основного клиентского кода. Для соблюдения этого принципа достаточно следовать правилу: программируйте на основу интерфейсов а не их реализации, о чем я и писал, в ранее упомянутой статье.
Резюме
В этой статье я попытался лайтово, не особо нагромождая ваше внимание большими кусками кода описать принципы SOLID в PHP, сделать его описание и привести полной объяснения его сути. Для того, чтобы научиться писать по SOLID-у, вам нужно просто практиковаться писать всё больше и больше, постоянно держа в голове эти принципы и стараясь их внедрять в нужные места. После чего, полное понимание и умение их применять придёт само собой.

В серці. Назавжди.
Вчора у мене помер однокласник. А сьогодні бабуся. І хто б міг уявити, що цей рік принесе війну, смерть товариша, та смерть члена сім’ї? Це боляче. Проте це добре нагадування про те, як швидко тече час. І як його ціна збільшується кожної марно витраченої секунди. І я не скажу щось
20 мая 2022 г. 1 min read

Ось такий він, руський мир
«Руський мир» — звучить дуже сильно та виправдовуюче. Гарна обгортка виправдання слабкості, аморальності та нікчемності своїх дійсних намірів. Руський мир, який дуже солодко звучить для всіх, хто хоче закрити очі на факт повномасштабної війни. Дуже добре виправдання вбивства для купки звірів. Втім, це ж росія, в якій все виглядає логічно
16 апр. 2022 г. 3 min read

Перехват запросов и ответов JavaScript Fetch API
Перехватчики — это блоки кода, которые вы можете использовать для предварительной или последующей обработки HTTP-вызовов, помогая в обработке глобальных ошибок, аутентификации, логирования, изменения тела запроса и многом другом. В этой статье вы узнаете, как перехватывать вызовы JavaScript Fetch API. Есть два типа событий, для которых вы можете захотеть перехватить HTTP-вызовы:
Принципы SOLID, о которых должен знать каждый разработчик
Объектно-ориентированное программирование принесло в разработку ПО новые подходы к проектированию приложений. В частности, ООП позволило программистам комбинировать сущности, объединённые некоей общей целью или функционалом, в отдельных классах, рассчитанных на решение самостоятельных задач и независимых от других частей приложения. Однако само по себе применение ООП не означает, что разработчик застрахован от возможности создания непонятного, запутанного кода, который тяжело поддерживать. Роберт Мартин, для того, чтобы помочь всем желающим разрабатывать качественные ООП-приложения, разработал пять принципов объектно-ориентированного программирования и проектирования, говоря о которых, с подачи Майкла Фэзерса, используют акроним SOLID.
Что такое SOLID?
Вот как расшифровывается акроним SOLID:
- S: Single Responsibility Principle (Принцип единственной ответственности).
- O: Open-Closed Principle (Принцип открытости-закрытости).
- L: Liskov Substitution Principle (Принцип подстановки Барбары Лисков).
- I: Interface Segregation Principle (Принцип разделения интерфейса).
- D: Dependency Inversion Principle (Принцип инверсии зависимостей).
Сейчас мы рассмотрим эти принципы на схематичных примерах. Обратите внимание на то, что главная цель примеров заключается в том, чтобы помочь читателю понять принципы SOLID, узнать, как их применять и как следовать им, проектируя приложения. Автор материала не стремился к тому, чтобы выйти на работающий код, который можно было бы использовать в реальных проектах.
Принцип единственной ответственности
«Одно поручение. Всего одно.» — Локи говорит Скурджу в фильме «Тор: Рагнарёк».
Каждый класс должен решать лишь одну задачу.
Класс должен быть ответственен лишь за что-то одно. Если класс отвечает за решение нескольких задач, его подсистемы, реализующие решение этих задач, оказываются связанными друг с другом. Изменения в одной такой подсистеме ведут к изменениям в другой.
Обратите внимание на то, что этот принцип применим не только к классам, но и к компонентам программного обеспечения в более широком смысле.
Например, рассмотрим этот код:
Класс Animal , представленный здесь, описывает какое-то животное. Этот класс нарушает принцип единственной ответственности. Как именно нарушается этот принцип?
В соответствии с принципом единственной ответственности класс должен решать лишь какую-то одну задачу. Он же решает две, занимаясь работой с хранилищем данных в методе saveAnimal и манипулируя свойствами объекта в конструкторе и в методе getAnimalName .
Как такая структура класса может привести к проблемам?
Если изменится порядок работы с хранилищем данных, используемым приложением, то придётся вносить изменения во все классы, работающие с хранилищем. Такая архитектура не отличается гибкостью, изменения одних подсистем затрагивают другие, что напоминает эффект домино.
Для того чтобы привести вышеприведённый код в соответствие с принципом единственной ответственности, создадим ещё один класс, единственной задачей которого является работа с хранилищем, в частности — сохранение в нём объектов класса Animal :
Вот что по этому поводу говорит Стив Фентон: «Проектируя классы, мы должны стремиться к тому, чтобы объединять родственные компоненты, то есть такие, изменения в которых происходят по одним и тем же причинам. Нам следует стараться разделять компоненты, изменения в которых вызывают различные причины».
Правильное применение принципа единственной ответственности приводит к высокой степени связности элементов внутри модуля, то есть к тому, что задачи, решаемые внутри него, хорошо соответствуют его главной цели.
Принцип открытости-закрытости
Программные сущности (классы, модули, функции) должны быть открыты для расширения, но не для модификации.
Продолжим работу над классом Animal .
Мы хотим перебрать список животных, каждое из которых представлено объектом класса Animal , и узнать о том, какие звуки они издают. Представим, что мы решаем эту задачу с помощью функции AnimalSounds :
Самая главная проблема такой архитектуры заключается в том, что функция определяет то, какой звук издаёт то или иное животное, анализируя конкретные объекты. Функция AnimalSound не соответствует принципу открытости-закрытости, так как, например, при появлении новых видов животных, нам, для того, чтобы с её помощью можно было бы узнавать звуки, издаваемые ими, придётся её изменить.
Добавим в массив новый элемент:
После этого нам придётся поменять код функции AnimalSound :
Как видите, при добавлении в массив нового животного придётся дополнять код функции. Пример это очень простой, но если подобная архитектура используется в реальном проекте, функцию придётся постоянно расширять, добавляя в неё новые выражения if .
Как привести функцию AnimalSound в соответствие с принципом открытости-закрытости? Например — так:
Можно заметить, что у класса Animal теперь есть виртуальный метод makeSound . При таком подходе нужно, чтобы классы, предназначенные для описания конкретных животных, расширяли бы класс Animal и реализовывали бы этот метод.
В результате у каждого класса, описывающего животного, будет собственный метод makeSound , а при переборе массива с животными в функции AnimalSound достаточно будет вызвать этот метод для каждого элемента массива.
Если теперь добавить в массив объект, описывающий новое животное, функцию AnimalSound менять не придётся. Мы привели её в соответствие с принципом открытости-закрытости.
Рассмотрим ещё один пример.
Представим, что у нас есть магазин. Мы даём клиентам скидку в 20%, используя такой класс:
Теперь решено разделить клиентов на две группы. Любимым ( fav ) клиентам даётся скидка в 20%, а VIP-клиентам ( vip ) — удвоенная скидка, то есть — 40%. Для того, чтобы реализовать эту логику, было решено модифицировать класс следующим образом:
Такой подход нарушает принцип открытости-закрытости. Как видно, здесь, если нам надо дать некоей группе клиентов особую скидку, приходится добавлять в класс новый код.
Для того чтобы переработать этот код в соответствии с принципом открытости-закрытости, добавим в проект новый класс, расширяющий класс Discount . В этом новом классе мы и реализуем новый механизм:
Если решено дать скидку в 80% «супер-VIP» клиентам, выглядеть это должно так:
Как видите, тут используется расширение возможностей классов, а не их модификация.
Принцип подстановки Барбары Лисков
Необходимо, чтобы подклассы могли бы служить заменой для своих суперклассов.
Цель этого принципа заключаются в том, чтобы классы-наследники могли бы использоваться вместо родительских классов, от которых они образованы, не нарушая работу программы. Если оказывается, что в коде проверяется тип класса, значит принцип подстановки нарушается.
Рассмотрим применение этого принципа, вернувшись к примеру с классом Animal . Напишем функцию, предназначенную для возврата информации о количествах конечностей животного.
Функция нарушает принцип подстановки (и принцип открытости-закрытости). Этот код должен знать о типах всех обрабатываемых им объектов и, в зависимости от типа, обращаться к соответствующей функции для подсчёта конечностей конкретного животного. Как результат, при создании нового типа животного функцию придётся переписывать:
Для того чтобы эта функция не нарушала принцип подстановки, преобразуем её с использованием требований, сформулированных Стивом Фентоном. Они заключаются в том, что методы, принимающие или возвращающие значения с типом некоего суперкласса ( Animal в нашем случае) должны также принимать и возвращать значения, типами которых являются его подклассы ( Pigeon ).
Вооружившись этими соображениями мы можем переделать функцию AnimalLegCount :
Теперь эта функция не интересуется типами передаваемых ей объектов. Она просто вызывает их методы LegCount . Всё, что она знает о типах — это то, что обрабатываемые ей объекты должны принадлежать классу Animal или его подклассам.
Теперь в классе Animal должен появиться метод LegCount :
А его подклассам нужно реализовать этот метод:
В результате, например, при обращении к методу LegCount для экземпляра класса Lion производится вызов метода, реализованного в этом классе, и возвращается именно то, что можно ожидать от вызова подобного метода.
Теперь функции AnimalLegCount не нужно знать о том, объект какого именно подкласса класса Animal она обрабатывает для того, чтобы узнать сведения о количестве конечностей у животного, представленного этим объектом. Функция просто вызывает метод LegCount класса Animal , так как подклассы этого класса должны реализовывать этот метод для того, чтобы их можно было бы использовать вместо него, не нарушая правильность работы программы.
Принцип разделения интерфейса
Создавайте узкоспециализированные интерфейсы, предназначенные для конкретного клиента. Клиенты не должны зависеть от интерфейсов, которые они не используют.
Этот принцип направлен на устранение недостатков, связанных с реализацией больших интерфейсов.
Рассмотрим интерфейс Shape :
Он описывает методы для рисования кругов ( drawCircle ), квадратов ( drawSquare ) и прямоугольников ( drawRectangle ). В результате классы, реализующие этот интерфейс и представляющие отдельные геометрические фигуры, такие, как круг (Circle), квадрат (Square) и прямоугольник (Rectangle), должны содержать реализацию всех этих методов. Выглядит это так:
Странный у нас получился код. Например, класс Rectangle , представляющий прямоугольник, реализует методы ( drawCircle и drawSquare ), которые ему совершенно не нужны. То же самое можно заметить и при анализе кода двух других классов.
Предположим, мы решим добавить в интерфейс Shape ещё один метод, drawTriangle , предназначенный для рисования треугольников:
Это приведёт к тому, что классам, представляющим конкретные геометрические фигуры, придётся реализовывать ещё и метод drawTriangle . В противном случае возникнет ошибка.
Как видно, при таком подходе невозможно создать класс, который реализует метод для вывода круга, но не реализует методы для вывода квадрата, прямоугольника и треугольника. Такие методы можно реализовать так, чтобы при их выводе выбрасывалась бы ошибка, указывающая на то, что подобную операцию выполнить невозможно.
Принцип разделения интерфейса предостерегает нас от создания интерфейсов, подобных Shape из нашего примера. Клиенты (у нас это классы Circle , Square и Rectangle ) не должны реализовывать методы, которые им не нужно использовать. Кроме того, этот принцип указывает на то, что интерфейс должен решать лишь какую-то одну задачу (в этом он похож на принцип единственной ответственности), поэтому всё, что выходит за рамки этой задачи, должно быть вынесено в другой интерфейс или интерфейсы.
В нашем же случае интерфейс Shape решает задачи, для решения которых необходимо создать отдельные интерфейсы. Следуя этой идее, переработаем код, создав отдельные интерфейсы для решения различных узкоспециализированных задач:
Теперь интерфейс ICircle используется лишь для рисования кругов, равно как и другие специализированные интерфейсы — для рисования других фигур. Интерфейс Shape может применяться в качестве универсального интерфейса.
Принцип инверсии зависимостей
Объектом зависимости должна быть абстракция, а не что-то конкретное.
- Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
- Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
В процессе разработки программного обеспечения существует момент, когда функционал приложения перестаёт помещаться в рамках одного модуля. Когда это происходит, нам приходится решать проблему зависимостей модулей. В результате, например, может оказаться так, что высокоуровневые компоненты зависят от низкоуровневых компонентов.
Здесь класс Http представляет собой высокоуровневый компонент, а XMLHttpService — низкоуровневый. Такая архитектура нарушает пункт A принципа инверсии зависимостей: «Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций».
Класс Http вынужденно зависит от класса XMLHttpService . Если мы решим изменить механизм, используемый классом Http для взаимодействия с сетью — скажем, это будет Node.js-сервис или, например, сервис-заглушка, применяемый для целей тестирования, нам придётся отредактировать все экземпляры класса Http , изменив соответствующий код. Это нарушает принцип открытости-закрытости.
Класс Http не должен знать о том, что именно используется для организации сетевого соединения. Поэтому мы создадим интерфейс Connection :
Интерфейс Connection содержит описание метода request и мы передаём классу Http аргумент типа Connection :
Теперь, вне зависимости от того, что именно используется для организации взаимодействия с сетью, класс Http может пользоваться тем, что ему передали, не заботясь о том, что скрывается за интерфейсом Connection .
Перепишем класс XMLHttpService таким образом, чтобы он реализовывал этот интерфейс:
В результате мы можем создать множество классов, реализующих интерфейс Connection и подходящих для использования в классе Http для организации обмена данными по сети:
Как можно заметить, здесь высокоуровневые и низкоуровневые модули зависят от абстракций. Класс Http (высокоуровневый модуль) зависит от интерфейса Connection (абстракция). Классы XMLHttpService , NodeHttpService и MockHttpService (низкоуровневые модули) также зависят от интерфейса Connection .
Кроме того, стоит отметить, что следуя принципу инверсии зависимостей, мы соблюдаем и принцип подстановки Барбары Лисков. А именно, оказывается, что типы XMLHttpService , NodeHttpService и MockHttpService могут служить заменой базовому типу Connection .
Итоги
Здесь мы рассмотрели пять принципов SOLID, которых следует придерживаться каждому ООП-разработчику. Поначалу это может оказаться непросто, но если к этому стремиться, подкрепляя желания практикой, данные принципы становятся естественной частью рабочего процесса, что оказывает огромное положительное воздействие на качество приложений и значительно облегчает их поддержку.
Еще больше полезной информации для программистов вы найдете на нашем сайте.
SOLID — Принцип открытости / закрытости
Второй принцип из списка принципов SOLID.
Классы (модули, функции) должны быть открыты для расширения и закрыты для изменения.
Иными словами приложение следует проектировать так чтобы для изменения поведения класса,
нам не потребовалось менять код самого класса.
Для демонстрации этого принципа продолжим модификацию примера из предыдущего принципа.
Дано:
$logger = new Logger(); $product = new Product($logger); $product->setPrice(10);
protected $logger; public function __construct(Logger $logger) < $this->logger = $logger; > public function setPrice($price) < try < // save price in db >catch (DbException $e) < $this->logger->log($e->getMessage()); > >
class Logger < private function saveToFile($message) < //. >public function log($message) < //. $this->saveToFile($message); > >
В данном примере мы имеем два класса. Класс Product который отвечает за работу с товаром.
И класс Logger цель которого логировать ошибки в текстовый файл.
Задача:
Сделать логирование не в текстовый файл, а в базу данных.
(либо логироваться в БД должен только класс Product, но другие классы, которые используют Logger,
должны логировать в файл как и прежде)
В данном примере, для того чтобы реализовать требования заказчика, мы нарушаем принцип открытости/закрытости.
Так как будем вынуждены модифицировать существующие классы.
Если функционал нашей системы не сложен, то этим можно пренебречь,
но если система у нас большая, то изменения в классах могут вызвать непредсказуемые ошибки.
Для того чтобы следовать принципу открытости/закрытости организовать нашу систему можно следующим образом.
interface ILogger
class FileLogger implements ILogger < private function saveToFile($message) < //. >public function log($message) < //. $this->saveToFile($message); > > class DBLogger implements ILogger < private function saveToDB($message) < //. >public function log($message) < //. $this->saveToDB($message); > >
protected $logger; public function __construct(ILogger $logger) < $this->logger = $logger; > public function setPrice($price) < try < // save price in db >catch (DbException $e) < $this->logger->log($e->getMessage()); > >
$logger = new DBLogger(); $product = new Product($logger); $product->setPrice(10);
- ← SOLID — Принцип единственной обязанности (ответственности)
- SOLID — Принцип подстановки Барбары Лисков →
Шпаргалка по SOLID-принципам с примерами на PHP
Тема SOLID-принципов и в целом чистоты кода не раз поднималась на Хабре и, возможно, уже порядком изъезженная. Но тем не менее, не так давно мне приходилось проходить собеседования в одну интересную IT-компанию, где меня попросили рассказать о принципах SOLID с примерами и ситуациями, когда я не соблюл эти принципы и к чему это привело. И в тот момент я понял, что на каком-то подсознательном уровне я понимаю эти принципы и даже могут назвать их все, но привести лаконичные и понятные примеры для меня стало проблемой. Поэтому я и решил для себя самого и для сообщества обобщить информацию по SOLID-принципам для ещё лучшего её понимания. Статья должна быть полезной, для людей только знакомящихся с SOLID-принципами, также, как и для людей «съевших собаку» на SOLID-принципах.
Для тех, кто знаком с принципами и хочет только освежить память о них и их использовании, можно обратиться сразу к шпаргалке в конце статьи.
Что же такое SOLID-принципы? Если верить определению Wikipedia, это:
аббревиатура пяти основных принципов дизайна классов в объектно-ориентированном проектировании — Single responsibility, Open-closed, Liskov substitution, Interface segregation и Dependency inversion.
- Принцип единственной ответственности (Single responsibility)
- Принцип открытости/закрытости (Open-closed)
- Принцип подстановки Барбары Лисков (Liskov substitution)
- Принцип разделения интерфейса (Interface segregation)
- Принцип инверсии зависимостей (Dependency Invertion)
Принцип единственной ответственности (Single responsibility)
Итак, в качества примера возьмём довольно популярный и широкоиспользуемый пример — интернет-магазин с заказами, товарами и покупателями.
Принцип единственной ответственности гласит — «На каждый объект должна быть возложена одна единственная обязанность». Т.е. другими словами — конкретный класс должен решать конкретную задачу — ни больше, ни меньше.
Рассмотрим следующее описание класса для представления заказа в интернет-магазине:
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()*. */> >
Как можно увидеть, данный класс выполняет операций для 3 различный типов задач: работа с самим заказом( calculateTotalSum, getItems, getItemsCount, addItem, deleteItem ), отображение заказа( printOrder, showOrder ) и работа с хранилищем данных( load, save, update, delete ).
К чему это может привести?
Приводит это к тому, что в случае, если мы хотим внести изменения в методы печати или работы хранилища, мы изменяем сам класс заказа, что может привести к его неработоспособности.
Решить эту проблему стоит разделением данного класса на 3 отдельных класса, каждый из которых будет заниматься своей задачей
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)*. */> >
Теперь каждый класс занимается своей конкретной задачей и для каждого класса есть только 1 причина для его изменения.
Принцип открытости/закрытости (Open-closed)
Данный принцип гласит — «программные сущности должны быть открыты для расширения, но закрыты для модификации» . На более простых словах это можно описать так — все классы, функции и т.д. должны проектироваться так, чтобы для изменения их поведения, нам не нужно было изменять их исходный код.
Рассмотри на примере класса OrderRepository .
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 => $orderID)); return $query->fetchObject('Order'); > public function save($order)*. */> public function update($order)*. */> public function delete($order)*. */> >
В данном случае хранилищем у нас является база данных. например, MySQL. Но вдруг мы захотели подгружать наши данные о заказах, например, через API стороннего сервера, который, допустим, берёт данные из 1С. Какие изменения нам надо будет внести? Есть несколько вариантов, например, непосредственно изменить методы класса OrderRepository , но этот не соответствует принципу открытости/закрытости, так как класс закрыт для модификации, да и внесение изменений в уже хорошо работающий класс нежелательно. Значит, можно наследоваться от класса OrderRepository и переопределить все методы, но это решение не самое лучше, так как при добавлении метода в OrderRepository нам придётся добавить аналогичные методы во все его наследники. Поэтому для выполнения принципа открытости/закрытости лучше применить следующее решение — создать интерфейc IOrderSource , который будет реализовываться соответствующими классами MySQLOrderSource , ApiOrderSource и так далее.
Интерфейс IOrderSource и его реализация и использование
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)*. */> >
Таким образом, мы можем изменить источник и соответственно поведение для класса OrderRepository , установив нужный нам класс реализующий IOrderSource , без изменения класса OrderRepository .
Принцип подстановки Барбары Лисков (Liskov substitution)
Пожалуй, принцип, который вызывает самые большие затруднения в понимании.
Принцип гласит — «Объекты в программе могут быть заменены их наследниками без изменения свойств программы». Своими словами я бы это сказал так — при использовании наследника класса результат выполнения кода должен быть предсказуем и не изменять свойств метод.
К сожалению, придумать доступного примера для это принципа в рамках задачи интернет-магазина я не смог, но есть классический пример с иерархией геометрических фигур и вычисления площади. Код примера ниже.
Пример иерархии прямоугольника и квадрата и вычислении их площади
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 .
Очевидно, что такой код явно выполняется не так, как от него этого ждут.
Но в чём проблема? Разве «квадрат» не является «прямоугольником»? Является, но в геометрических понятиях. В понятиях же объектов, квадрат не есть прямоугольник, поскольку поведение объекта «квадрат» не согласуется с поведением объекта «прямоугольник».
- Предусловия не могут быть усилены в подклассе.
- Постусловия не могут быть ослаблены в подклассе.
«Что ещё за пред- и постусловия?» — можете спросите Вы.
Ответ: предусловия – это то, что должно быть выполнено вызывающей стороной перед вызовом метода, постусловия – это то, что, гарантируется вызываемым методом.
Вернёмся к нашему примеру и посмотрим, как мы изменили пред- и постусловия.
Предусловия мы никак не использовали при вызове методов установки высоты и ширины, а вот постусловия в классе-наследнике мы изменили и изменили на более слабые, чего по принципу Лисков делать было нельзя.
Ослабили мы их вот почему. Если за постусловие метода setWidth принять (($this->width == $width) && ($this->height == $oldHeight)) ( $oldHeight мы присвоили вначале метода setWidth), то это условие не выполняется в дочернем классе и соответственно мы его ослабили и принцип Лисков нарушен.
Поэтому, лучше в рамках ООП и задачи расчёта площади фигуры не делать иерархию «квадрат» наследует «прямоугольник», а сделать их как 2 отдельные сущности:
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; > >
Хороший реальный пример несоблюдения принципа Лискоу и решения, принятого в связи с этим, рассмотрен в книге Роберта Мартина «Быстрая разработка программ» в разделе «Принцип подстановки Лискоу. Реальный пример».
Принцип разделения интерфейса (Interface segregation)
Данный принцип гласит, что «Много специализированных интерфейсов лучше, чем один универсальный»
Соблюдение этого принципа необходимо для того, чтобы классы-клиенты использующий/реализующий интерфейс знали только о тех методах, которые они используют, что ведёт к уменьшению количества неиспользуемого кода.
Вернёмся к примеру с интернет-магазином.
Предположим наши товары могут иметь промокод, скидку, у них есть какая-то цена, состояние и т.д. Если это одежда то для неё устанавливается из какого материала сделана, цвет и размер.
Опишем следующий интерфейс
interface IItem
Данный интефейс плох тем, что он включает слишком много методов. А что, если наш класс товаров не может иметь скидок или промокодов, либо для него нет смысла устанавливать материал из которого сделан (например, для книг). Таким образом, чтобы не реализовывать в каждом классе неиспользуемые в нём методы, лучше разбить интерфейс на несколько мелких и каждым конкретным классом реализовывать нужные интерфейсы.
Разбиение интерфейса IItem на несколько
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)*. */> >
Принцип инверсии зависимостей (Dependency Invertion)
Принцип гласит — «Зависимости внутри системы строятся на основе абстракций. Модули верхнего уровня не зависят от модулей нижнего уровня. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций». Данное определение можно сократить — «зависимости должны строится относительно абстракций, а не деталей».
Для примера рассмотрим оплату заказа покупателем.
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)*. */>>
Всё кажется вполне логичным и закономерным. Но есть одна проблема — класс Customer зависит от класса OrderProcessor (мало того, не выполняется и принцип открытости/закрытости).
Для того, чтобы избавится от зависимости от конкретного класса, надо сделать так чтобы Customer зависел от абстракции, т.е. от интерфейса IOrderProcessor . Данную зависимость можно внедрить через сеттеры, параметры метода, или Dependency Injection контейнера. Я решил остановится на 2 методе и получил следующий код.
Инвертирование зависимости класса Customer
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)*. */>>
Таким образом, класс Customer теперь зависит только от абстракции, а конкретную реализацию, т.е. детали, ему не так важны.
Шпаргалка
- Принцип единственной ответственности (Single responsibility)
«На каждый объект должна быть возложена одна единственная обязанность»
Для этого проверяем, сколько у нас есть причин для изменения класса — если больше одной, то следует разбить данный класс. - Принцип открытости/закрытости (Open-closed)
«Программные сущности должны быть открыты для расширения, но закрыты для модификации»
Для этого представляем наш класс как «чёрный ящик» и смотрим, можем ли в таком случае изменить его поведение. - Принцип подстановки Барбары Лисков (Liskov substitution)
«Объекты в программе могут быть заменены их наследниками без изменения свойств программы»
Для этого проверяем, не усилили ли мы предусловия и не ослабили ли постусловия. Если это произошло — то принцип не соблюдается - Принцип разделения интерфейса (Interface segregation)
«Много специализированных интерфейсов лучше, чем один универсальный»
Проверяем, насколько много интерфейс содержит методов и насколько разные функции накладываются на эти методы, и если необходимо — разбиваем интерфейсы. - Принцип инверсии зависимостей (Dependency Invertion)
«Зависимости должны строится относительно абстракций, а не деталей»
Проверяем, зависят ли классы от каких-то других классов(непосредственно инстанцируют объекты других классов и т.д) и если эта зависимость имеет место, заменяем на зависимость от абстракции.
Надеюсь, моя «шпаргалка» поможет кому-нибудь в понимании принципов SOLID и даст толчок к их использованию в своих проектах.
Спасибо за внимание.
P.S. В комментариях посоветовали хорошую книгу — Роберт Мартин «Быстрая разработка программ». Там очень подробно и с примерами описаны принципы SOLID.