Автор: Sergey Teplyakov

Приложите все усилия для того, чтобы вы управляли зависимостями в приложении, а не зависимости управляли вами.
Мнение автора статьи по поводу зависимостей

Основной способ борьбы со сложностью сводится к тому, чтобы в один момент времени мы могли сосредоточиться на минимальном количестве сущностей или абстракций. В основном этот принцип исходит из двух предпосылок: во-первых, средний человек не в состоянии держать в голове одновременно слишком большое количество понятий (обычно говорится о цифре семь, плюс минус две) и при увеличении их количества приходится свопить недостающую информацию и анализировать каждый из ее кусков повторно. Во-вторых, при увеличении количества сущностей, которыми мы оперируем в один момент времени, увеличивается и суммарная сложность их поведения: достаточно попытаться объединить в одном месте кэширование и обычную бизнес логику, чтобы понять, что это значит.

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

Все известные ООП-шные принципы, включая Принцип Единой Ответственности, понятия связности и связанности (cohesion и coupling) и многие другие, призваны бороться с неотъемлемой сложностью (essential complexity) нашей бизнес-области и сводить случайную сложность (accidental complexity) к минимуму. Все наши продуманные абстракции, хитроумные паттерны и высокоуровневые языки программирования призваны акцентировать внимание на естественной сложности задачи, скрывая на уровень ниже несущественные подробности без которых можно обойтись.

Стабильные и изменчивые зависимости

У каждого из нас есть интуитивное представление о том, что является зависимостью и что ею не является. Так, например, с параноидальной точки зрения любая сущность, используемая нашим классом, является зависимостью, включая примитивные типы языка программирования и другие типы стандартной библиотеки. С другой стороны, очевидно, что даже в стандартной библиотеке есть классы, использование которых напрямую может завязать наш класс слишком тесно с некоторым окружением и сделает невозможным, например, модульное тестирование.

Марк Cиман (Mark Seeman) в своей книге Dependency Injection in .NET именно по этой причине выделяет два типа зависимостей: с одной стороны у нас есть стабильные зависимости (stable dependencies), абстрагироваться от которых нет особого смысла, поскольку они доступны из коробки, являются стандартом де факто в вашей команде и их поведение не меняется в зависимости от состояния окружения.

Кроме этого, есть изменчивые зависимости (volatile dependencies) реализация (или даже публичный интерфейс) которых может измениться в будущем, либо их поведение может измениться самостоятельно без нашего ведома, поэтому их нужно выделить в отдельную абстракцию и ограничить их использование. К такому классу зависимостей относятся третисторонние библиотеки, собственный код, область применения которого хорошо бы ограничить. К этому же типу зависимости относятся зависимости, завязанные на текущее окружение, такие как файлы, сокеты, базы данных и так далее. И хотя интерфейс таких зависимостей едва ли изменится в будущем, их изменчивость проявляется в том, что их поведение может измениться без изменения пользовательского кода.

СОВЕТ
На самом деле, ограничение использования любой зависимости является идеальным способом борьбы с изменениями. Ведь чем в меньшем количестве мест в вашем коде используется некоторый класс или интерфейс, тем меньше стоимость его изменения или ошибки в его реализации. Если вы считаете, что нечто может измениться в будущем, то сделайте это деталью реализации, сведя, таким образом, область изменений к минимуму.

И хотя названия этих двух типов зависимостей с первого взгляда может показаться не вполне точным, лично меня эти термины вполне устраивают. Главное, чтобы это разделение было у нас в голове: есть зависимости, от которых нужно абстрагироваться, а есть такие которые можно использовать напрямую и в этом ничего страшного!

Кроме того, одну и ту же зависимость (например, все тот же FileStream) можно в одном контексте рассматривать как стабильную, а в другом как изменчивую. С другой стороны, само использование класса FileStream говорит о некотором персистентном хранилище или чем-то подобным. Возможно, разумнее будет абстрагироваться не просто от файловых операций, а выделить некоторую бизнес-сущность, типа IConfigurationLoader и протаскивать уже ее, а не низкоуровневые сущности, типа IStream или кастомный IFileStream.

Зачем выделять зависимости?

Каждый раз, когда мы выделяем зависимость в самостоятельную абстракцию, мы преследуем определенную цель (или несколько целей).

В-первых, путем выделения зависимости мы можем повысить уровень абстракции и оградить весь остальной код от ненужных подробностей. Для этого, например, мы можем выделить отдельные интерфейсы/ классы для доступа к удаленным сервисам, слою доступа данных и т.п. Мы просто хотим думать не в терминах реализации (WCF, NHibernate), а в терминах абстракции (AbstractService, CustomRepostory). Не нужно быть гуру в современных новомодных принципах разработки, чтобы прийти к выводу, что подобные операции стоит спрятать куда-нибудь поглубже и не размазывать их использование ровным слоем по всему приложению.

Во-вторых, возможно мы хотим получить конфигурируемость поведения путем использования паттернов Стратегия, Абстрактная фабрика и тому подобных. Т.е. мы с самого начала знаем, что у нас будет несколько реализаций и создавать соответствующий дизайн с самого начала.

ПРИМЕЧАНИЕ
Марк Сииман в своей книге приводит очень интересное наблюдение, подтвержденное и моей собственной практикой. Многие разработчики и архитекторы забывают, что у любого решения есть и обратная сторона и гибкость в этом вопросе не исключение. На самом деле, не такое уж много приложений требует гибкой настройки своего поведения путем переконфигурирования приложения. Обычно повторное развертывание приложение с заменой текущей реализации может быть достаточно гибким решением, особенно когда это развертывание происходит на собственных серверах. Поэтому прежде чем закладываться на конфигурябельность всего и вся подумайте о том, насколько это вам действительно нужно.

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

Интерфейс != слабосвязанный дизайн

В последнее время мода использования интерфейсов стала настолько популярной, что очень часто их начинают выделять автоматом, считая это залогом слабосвязанного дизайна и тестируемости приложения. И хотя правильное выделение зависимостей и использование абстракций (интерфейсов и абстрактных классов), а не конкретных реализаций (конкретных классов) и правда приводит к слабой связанности (low coupling) чрезмерное их использование может привести к обратному результату.

Сколько раз вы видели класс, принимающий 10 зависимостей в конструкторе? Или класс, который принимает в конструкторе сервис-локатор или DI-контейнер, и дергает потом из него зависимости при необходимости? И хотя в таком случае мы не завязываемся на конкретные классы, такой дизайн едва ли можно назвать слабосвязанным, ведь для того, чтобы разобраться в том, что же он требует на вход, придется проанализировать всю его реализацию.

Скотт Мейерс (гуру С++) самым важным принципом проектирования считает следующий: ваш класс или модуль должно быть легко использовать правильно и сложно использовать неправильно. Классом же с десятком параметром правильно пользоваться весьма сложно, точнее достаточно просто пользоваться в вашем давно отконфигурированном приложении с использованием DI контейнеров, но создать и угодить ему с нуля дело совсем не простое.

Именно поэтому использование сервис локатора или внедрение обязательных зависимостей через свойства являются антипаттернами. В этом случае невероятно просто использовать класс неправильно и сложно использовать правильно.

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

Даже если ваш класс завязан лишь на абстракции (т.е. на интерфейсы) это не значит, что связанность отсутствует, просто вместо явной зависимости от конкретных реализаций, вы получили неявную зависимость (implicit coupling) на множество абстракций, на порядок их создания, на их состояние в момент использования, на определенные детали их реализации, на стабильность их интерфейсов, что делает сложным понимание того, как же вся эта хрень работает.

Выделение зависимости в интерфейс добавляет в приложение шов (seam) благодаря которому мы можем вклиниться в существующее поведение. Подобные швы дают определенную гибкость, но делают это не бесплатно если швов слишком много, то мы получаем слишком сложное решение, более сложное в понимании и сопровождении.

Заключение

Зависимости сами по себе не являются чем-то плохим, пока они используются в ограниченном количестве мест, позволяют легко сопровождать код и писать для него юнит тесты, и основной способ борьбы с ними сводится не столько к выделению интерфейсов, сколько к ограничению их области применения. При этом помните, что параноидальное выделение интерфейсов и протаскивание их десятками с помощью контейнеров не делает дизайн слабсвязанным и понятным. Код остается формально тестируемым, но стоимость его сопровождения (и стоимость сопровождения тестов) будет не меньше, чем исходный якобы монолитный дизайн с мелкими конкретными классами.

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

В следующий раз мы перейдем к практическим аспектам управления зависимостями, поговорим о роли контейнеров, о тестируемости без выделения интерфейсов и о многом другом.

Помогла статья? Оцените её!
0 из 5. Общее количество голосов - 0
 

You have no rights to post comments

Дмитрий Крикунов

Публикую статьи, обучающие курсы и новости по программированию: алгоритмам, языкам (С++, Java), параллельному программированию, паттернам и библиотекам (Qt, boost).