Что такое принципы SOLID? Объясняем на котиках
Каждый программист точно слышал о принципах SOLID. О них любят вспоминать преподаватели в вузах и интервьюеры на собеседованиях. Но ведь придумали их явно не для студентов и соискателей. Мы в Selectel любим котиков и в этой статье попробуем объяснить на них принципы SOLID.
Используйте навигацию, если не хотите читать текст полностью:
Принцип единственности ответственности
Для игры и еды должны быть отдельные места.
Принцип единственности ответственности (Single Responsibility Principle, SRP) гласит: каждый класс или модуль должен иметь только одну причину для изменения. То есть он должен выполнять только одну задачу.
Вроде звучит несложно, но где применить этот принцип? Да и что это вообще значит — «иметь только одну причину для изменения»? Разберемся на кошачьем примере.
Моего котика зовут Боря. Сейчас я попробую его имплементировать.
Борян у нас парень продвинутый — и помяукать может, и в базу себя сохранить. Допустим, ветеринар сказал Боре есть влажный корм только с утра. Давайте тогда разделим функцию eat на завтраки и остальные приемы пищи:
Теперь допустим, одной из игрушек нашего здоровяка станет утренняя еда со стола. Чтобы отразить это в коде, разделим функцию play, как выше сделали это с функцией eat:
Уже сейчас видно, что код становится не очень-то понятным. Если не знать историю его написания, трудно догадаться, чем morning_eat отличается от morning_eat_play. Теперь сделаем нашего котяру чуть более солидным — объединим методы с похожей тематикой в классы. Те, что связаны с едой (morning_eat и eat) положим в класс CatFeeding. Таким образом, этот класс будет иметь только одну причину для изменения — кормежку. Аналогично, функции morning_eat_play и play перенесем в класс CatPlay, а save_to_database — в CatDatabase.
Отлично, теперь каждый из классов имеет только одну причину для изменения — кормежка, игры или сохранение в БД! Но тут главное — знать меру и не наплодить классов под каждый метод.
Принцип открытости / закрытости
Если кот научится шипеть, он не должен разучиться мяукать.
Принцип открытости / закрытости (Open/Closed Principle, OCP) — программные сущности (классы, модули, функции и т. д.) должны быть открыты для расширения, но закрыты для модификации.
Расширения, модификации, бла-бла-бла… Сейчас с помощью Бори во всем разберемся! Одарим нашего парнишку речью:
Немного пожив с ним, я поняла, что у него с утра есть два настроения: ласковый милашка и киборг-убийца. Будет странно, если и в том, и в другом случае он будет болтать одинаково. Давайте попробуем усовершенствовать Боряна:
Мы видим, как наша функция становится больше, в ней становится легче сделать ошибку. Если у Бори появится еще какое-то настроение, то функция будет разрастаться. Давайте накинем solidности.
Посмотрите, как теперь выглядят наши классы. Они открыты для добавления новой логики, но закрыты для изменения текущей. Изменение исходной логики — очень опасная процедура. Метод может использоваться в разных местах в коде, при изменении очень трудно найти все ошибки. А вот если мы будем не изменять изначальную логику, а добавлять новые кейсы, работающий код не сломается, и мы сможем реализовать новый функционал без проблем!
Принцип подстановки Барбары Лисков
Если мы любим игрушки, то и мышек, и фантики.
Принцип подстановки Барбары Лисков (Liskov Substitution Principle, LSP) утверждает, что объекты подкласса должны быть взаимозаменяемыми с объектами суперкласса без изменения желаемых свойств программы. Это означает, что если класс S является подклассом класса T, то объекты класса T должны быть заменяемыми объектами класса S без нарушения корректности программы.
Пожалуй, самый непонятный принцип из всех. Когда я прочла его впервые, почувствовала себя на матане на первом курсе. Какие S? какие T? Классы, подклассы… Вот не сиделось же тебе, Лисков, на месте. Но готовьтесь, сейчас мы победим этот принцип раз и навсегда!
Давайте сделаем класс Бориных вещичек. В нем создадим метод, который будет возвращать, как Боря радуется и любит свои игрушки.
У Бори есть фантик, мои руки и все, что лежит на столе. Все это — его вещи, поэтому они должны наследоваться от BoryasStuff.
Недавно я купила Боре дорогущую мышь, которая умеет бегать от него и издавать звуки. Но, как водится, чем дороже игрушка, тем меньше она Борю интересует. Так что имплементация ее будет выглядеть так:
Вот здесь и нарушается принцип Барбары Лисков. Дело в том, что согласно нему мы должны уметь пользоваться всеми дочерними классами так же, как и родительскими. Но в классе BoryasStuff в отличие от дочернего класса CoolMouse метод enjoy выполняется без проблем.
Если мы можем вызвать BoryasStuff().enjoy(), то у нас должна быть возможность вызвать и CoolMouse().enjoy(). Для соблюдения принципа мы можем или исключить метод enjoy из BoryasStuff, или не наследовать CoolMouse от BoryasStuff, вот и все!
Принцип разделения интерфейсов
Незачем тебе умение плавать, если ты никогда не будешь это делать.
Принцип разделения интерфейсов (Interface Segregation Principle, ISP) гласит, что клиенты не должны зависеть от интерфейсов, которые они не используют.
Как-то мы мелочимся с выборами классов. Давайте создадим общий интерфейс котов. Что они умеют? Бегать, плавать, ну и, конечно же, орать в 5 утра.
Но вот мой Борик — домашний крепыш. Однажды плавал в ванной и ему очень не понравилось, мы решили его не мучить. Но если мы наследуем Борю от Cat, теоретически кто-то может заставить его плавать, чего ему явно не хотелось бы. Давайте разделим интерфейсы, чтобы мы могли наследовать только то, что нужно:
Теперь все в порядке! Можем просто наследовать Борю от Walkable и Hateable. В этом и есть принцип разделения интерфейсов. Мы не должны наследоваться от того, что не используем.
Принцип инверсии зависимостей
То, что коты мяукают, не влияет на всех остальных зверей. Но то, что животные издают звуки, влияет на котов.
Принцип инверсии зависимостей (Dependency Inversion Principle) гласит:
«Модули верхнего уровня не должны зависеть от модулей нижнего. Оба должны зависеть от абстракций»,
«Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций».
Так много слов и так трудно найти в них смысл. Разберем на примере использование этого принципа. Большинство животных издает какие-то звуки. Давайте напишем интерфейс для этого:
Теперь давайте определим кошачий звук:
Смотрите, кошачий звук наследуется от звуков животных. Все выглядит логично. Теперь давайте в очередной раз имплементируем Борю:
мы в какой-то момент решим, что Боря не кот, а, например, крокодил, нам не нужно будет переписывать класс BoryaCoolCat. Достаточно просто передать в него любой другой класс, который наследуется от Sound!
Этот принцип очень неплохо работает в больших проектах. Тут на помощь приходит DI-контейнер. Возможно, в следующих статьях затрону эту занятную тему.
Заключение
В завершение нашего solidного путешествия по принципам и мискам хочется подчеркнуть, что все это — не просто набор правил. Да, ты можешь выучить их к экзамену или собесу и не вспоминать больше. Но они и правда помогают создавать более качественный и поддерживаемый код.
Хороший код, как и котики, требует внимания и заботы. Применяя принципы SOLID, мы не только улучшаем его структуру, но и делаем более понятным для других разработчиков (а также для нас самих в будущем). В конечном итоге, это позволяет сосредоточиться на решении задач, а не на борьбе с последствиями наших костылей.
Создание качественного кода — это не просто задача, а искусство. Надеюсь, эта статья вдохновила вас взглянуть на принципы SOLID с новой стороны и начать применять их в своей практике.
Доводилось ли вам сталкиваться с использованием принципов SOLID? Считаете ли вы их чем-то обязательным или, скорее, факультативным? Делитесь своим мнением в комментариях.