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

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 id = ?";
 
        $post = $this->dbRawSql($db, $sql_query, $post_id);
       
        if (!empty($_POST['title'])) {
            $this->editPost($post, $_POST['title']);
            echo "

Успешно сохранено!

"; return true; } echo "<h1>Пост #" . e($post->id). ": " . e($post->title) . "</h1>"; echo "<p>Для редактирования заполните эту форму</p>"; echo "<form>"; echo "<input type='text' name='title' value='" . e($post->title) . "'>"; echo "<input type=submit>"; echo "</form>"; 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 = ?", $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-у, вам нужно просто практиковаться писать всё больше и больше, постоянно держа в голове эти принципы и стараясь их внедрять в нужные места. После чего, полное понимание и умение их применять придёт само собой.