Автор: Sergey Teplyakov

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

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

Тэд Фейсон (Ted Faison) в своей книге Event-Based Programming вводит аксиому управления зависимостями, которая звучит следующим образом:

Чем сложнее класс или компонент, тем меньше у него должно быть внешних связей.
(The more complex a class or component is, the more decoupled it should be)

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

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

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

В этом случае, нет никакого смысла отвязывать класс Bond (облигация) от конкретного поставщика данных, заменяя SqlRepository на IRepository, нам нужно весь аспект работы с персистентным (саму зависимость как таковую) вынести на более высокий уровень:

// ПЛОХО! Класс облигации использует конкретный класс SqlRepository
class Bond1
{
private readonly SqlRepository _repository = new SqlRepository(),
public Bond1()
{
var bondData = _repository.GetBondData(),
}
}
// ПЛОХО! Класс облигации использует 'абстракцию' IRepository
class Bond2
{
private readonly IRepository _repository,
public Bond2(IRepository repository)
{
_repository = repository,
var bondData = _repository.GetBondData(),
}
}
// ХОРОШО! Класс облигации ничего не знает о слое доступа к данным!
class Bond3
{
public Bond3(BondData bondData)
{}
}

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

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

Метрика стабильности класса

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

image

где Ce количество исходящих связей (efferent coupling), а Ca количество входящих связей (afferent coupling), класс абсолютно стабилен при Ce, равном 0.

Из этой формулы вытекает два следствия: (1) чем больше у класса зависимостей, тем менее стабильным он является и (2) чем большим количеством классов он используется, тем больше вреда может быть при его изменении.

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

Еще в далеких девяностых Гради Буч писал следующее:

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

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

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

Заключение

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

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

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

You have no rights to post comments

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

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