GrabDuck

Почему я не преподаю SOLID и «принцип устранения зависимостей»

:

Статья 1. Почему я не преподаю SOLID


Если вы разговариваете с кем-то, кому небезразлично качество кода, уже достаточно скоро в разговоре всплывёт SOLID — аббревиатура, помогающая разработчикам запомнить пять важных принципов объектно-ориентированного программирования:

SOLID полезен. Его разработали знатоки в нашей области. Он помогает людям рассуждать о дизайне. Помогает создавать системы, устойчивые к изменениям.

Раньше SOLID был краеугольным камнем моего набора средств проектирования. Я делал все возможное, чтобы сделать мой код как можно более SOLID. Я учил других поступать так же.

Сегодня SOLID остается для меня важным, но я больше не пытаюсь сделать мой код SOLID. Я редко упоминаю его, когда говорю про дизайн. И тем более я не учу пользоваться им разработчиков, которым хочется почерпнуть хорошие дизайнерские методы проектирования. Он больше не находится у меня под рукой в моем «ящике для инструментов». Он лежит в пыльной коробке на чердаке. Я храню его, потому что он важен, но редко им пользуюсь.

SOLID создает проблемы. Достаточно большие проблемы, чтобы убрать его подальше на чердак. Когда я продвигал SOLID, те, кто были против его использования, указывали на эти проблемы. Я же пренебрегал мнением этих людей, называя их скептиками, которые сопротивляются переменам, или же не заинтересованными в качестве кода. Теперь же я убедился, что они были правы с самого начала.

Если бы меня попросили описать все эти проблемы одним словом, я бы выбрал слово непонятный. Разработчики, применяющие SOLID (в том числе я), часто создают непонятный код. Этот SOLID-код имеют низкий уровень связности, его несложно протестировать. Но он непонятный. И часто совсем не настолько адаптируемый, как хотел бы разработчик.

Основная проблема в том, что SOLID концентрируется на зависимостях. Каждый из принципов Открытости / Закрытости, Разделения интерфейса и Инверсии зависимостей ведет к использованию большого количества зависимостей от абстракций (интерфейса или абстрактного класса C#/Java). Принцип открытости / закрытости использует абстракции для простоты расширения. Принцип разделения интерфейса способствует созданию более клиенто-ориентированных абстракций. Принцип инверсии зависимостей говорит, что зависимости должны быть от абстракции, а не от конкретной реализации.

В результате все это приводит к тому, что разработчики начинают создавать интерфейсы где попало. Они загромождают свой код интерфейсами типа IFooer, IDoer, IMooer, IPooer.

Навигация превращается в кошмар. Во время code-review часто непонятно, какая именно часть кода заработает. Но это нормально. Это же SOLID. Это великолепный дизайн!

Чтобы помочь управиться с этим безумием, мы внедряем IoC-контейнер. А еще Mock-фреймвок для тестов. Если и раньше все было не слишком понятно, теперь становится непонятно окончательно. Теперь вы в самом прямом смысле не можете найти вызов конструктора в коде. Пытаетесь разобраться во всем этом? Удачи! Но это ничего. Потому что это же SOLID. Это великолепный дизайн!

Раз это такой великолепный дизайн, почему же он тогда такой непонятный? Разве это великолепный дизайн, если разработчики не могут просто и ясно объяснить его? Мне так не кажется. SOLID важен и полезен. Но я не думаю, что разработчики хорошо с ним справляются. Вот именно поэтому я больше не учу использовать SOLID.

Статья 2. За рамками SOLID: «Принцип устранения зависимостей»

Воспринимайте зависимости как видимые проблемы в коде

Согласно Принципу устранения зависимостей, мы меняем наши настрой по умолчанию и начинаем воспринимать зависимости как видимые проблемы в коде. Это не значит, что у нас совсем не будет зависимостей. Это просто значит, что наш настрой по умолчанию — воспринимать их как проблемы. Новый настрой заставит нас вникнуть в то, зачем нам требуется каждая зависимость, и устранить ее.

Мы получаем какие-то данные через зависимость? Передайте сами данные, но уберите ПолучательДанных.
Мы передаем что-то через зависимость? Рассмотрите модель с событиями и подпиской на них, вместо передачи интерфейса. Или же используйте Data Transfer Object, описывающий состояние зависимости и реагируйте на его изменение (как, например, в MVVM).

Объекты-значения

В большинстве проектов оказывается слишком много зависимостей, потому что они слабо используют объекты-значения для описания своих концепций. Следующие несколько простых подсказок помогут создать ясный и понятный код с умеренным количеством зависимостей и абстракций (подсмотрено в статье «Take on the 4 Rules of Simple Design» J.B. Rainsberger):
  • Избавьтесь от Одержимости примитивами
  • Придумывая названия, используй имена существительные (и не используй отглагольные существительные, которые имеют окончание “er”)
  • Избавляйтесь от дублирования

Применяя эти простые принципы, вы будете просто в шоке, насколько быстро ваш код станет простым, читабельным, тестопригодным и расширяемым, но при этом без лишнего захламления интерфейсами.

Тестопригодность в качестве эталона

Давайте взглянем на Принцип устранения зависимостей с точки зрения тестопригодности. Код с зависимостями (даже если эти зависимости незначительны) сложнее протестировать, чем код без них. Вот уровни, которые я расположил в порядке возрастания от простого к сложному (кажется, я нашел это в посте какого-то другого блога, но, хоть убей, не могу его найти):
  • Уровень 0: статическая функция без побочных эффектов
  • Уровень 1: Класс с неизменяемым состоянием. Например, Объекты-Значения, заменяющие примитивы вроде EmailAddress или PhoneNumber
  • Уровень 2: класс с изменяемым состоянием, взаимодействующий только с простыми зависимостями, такими как Объект-Значение из Уровня 1.
  • Уровень 3: класс с изменяемым состоянием, взаимодействующий с другими изменяемыми классами

Уровень 0 тестировать элементарно. Просто отправляйте различные входные данные в функцию и проверяйте правильность результата.

Уровень 1 не слишком отличается от Уровня 0, кроме того, что у вас есть еще несколько методов, которыми его можно протестировать и еще несколько конфигураций, которые тоже надо проверить. Уровень 1 хорош, потому что он заставляет вас инкапсулировать ваши концепции в Объекты-Значения.

Уровень 2 сложнее, чем Уровень 1, так как вам придется думать о внутреннем состоянии и тестировать разные случаи, в которых оно меняется. Но иногда вам нужен Уровень 2 из-за своих преимуществ.

Уровень 3 тестировать сложнее всего. Придется либо использовать Mock-объекты, либо тестировать несколько модулей одновременно.

Я хочу сделать тестирование как можно более простым, поэтому я стараюсь использовать как можно более низкий уровень, который удовлетворяет моим требованиям: в основном я использую код Уровня 0 и Уровня 1. Иногда Уровня 2 и редко Уровня 3. Мой код становится очень функциональным, однако пользуется преимуществом объектно-ориентированности при создании Объектов-Значений для группировки связанной функциональности.

Возвращаясь обратно к SOLID

Допустим, мы применили Принцип устранения зависимостей. Давайте проанализируем насколько SOLID стал этот код:
  • Принцип единственной обязанности: Ну, да! Массовое использование Объектов-Значения. Необыкновенно высокое сцепление.
  • Принцип открытости /закрытости: Да, но по-другому. Открытость в том, что мы можем как угодно комбинировать Объекты-Значения, а не в том, чтобы везде внедрять зависимости от абстракций.
  • Принцип подстановки Барбары Лисков: он нам не важен. Мы вообще почти не используем механизм наследования.
  • Принцип разделения интерфейса: опять же, не важен. Мы почти не используем интерфейсы.
  • Принцип инверсии зависимостей: опять же по большей части не нужен, так как мы устранили большую часть зависимостей. Оставшиеся зависимости являются Объектами-Значениями, которые стоит рассматривать как часть системы типов, а также возможно небольшое количество интерфейсов, чтобы общаться с внешним миром.
Все дело в Принципе единой обязанности

Применяя Принцип устранения зависимостей, вы фокусируетесь исключительно на Принципе единой обязанности. И вы получаете гибкость Принципа открытости / закрытости, который ведет к простоте адаптации под бизнес-требования. А еще вы можете забыть про все сложности, который несут с собой принципы подстановки Лисков, Разделения интерфейсов и Инверсии зависимостей. Мы выигрываем по всем статьям!