четверг, 28 февраля 2013 г.

Аксиома управления зависимостями

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

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

Тэд Фейсон (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) чем большим количеством классов он используется, тем больше вреда может быть при его изменении.

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

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

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

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

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

Заключение

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

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

13 комментариев:

  1. Это что получается что реализация DI по 2 примеру, делает проект очень зависимым. И почему я об это раньше на задумался .... или все же это хороший пример в определенных обстоятельствах ?

    ОтветитьУдалить
  2. @Александр: тут важно не то, как именно передается зависимость, а сам факт ее существования.

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

    А если кратко, то да, в этом случае мы получаем ненужную зависимость, делая проект очень зависимым, несмотря на использования DI;)

    ОтветитьУдалить
  3. ИМХО, пример с внедрением IRepository - неудачный. И вот почему. Ты из объекта с поведением сделал дата объект. А кто у тебя будет создавать дата объект? Класс выше будет знать и о Repository и о Bond. Если у тебя в классе определено поведение - не вижу ничего плохого для передачи IRepository. Если у тебя дата объект - ни о каком репозитории речь не идет. Так что надо определиться изначалаьно - что делаем?

    ОтветитьУдалить
  4. @eugene: у меня в классе Bond определено поведение вычисления стоимости (прошлой, будущей и еще где-то 27 других, ибо Bond - очень сложный объект по своей природе). Поэтому я не сделал из него дата-объект. Я сделал из него доменный объект, который знает о логике вычисления стоимости и не знает о слое персистентности.

    Я же определился, что мы делаем изначально: Bond - считает свою стоимость (причем считает весьма сложно). Это не BondService, который является медиатором между бизнес-объектом и базой данных, это и есть бизнес-объект (не путать с дата-объектом).

    ОтветитьУдалить
  5. Опис даних і маніпуляція над ними мають бути розділеними. Для SQL це аксіома вже 40 років — в DDL описуємо сутність, в DML маніпулюємо.

    ОтветитьУдалить
  6. @Андрей: речь не идет о разделении бизнес логики от слоя доступа, вопрос в том, сколько классов должны знать о существовании этого слоя.

    ОтветитьУдалить
  7. @Sergey. Я не знаю, может ты меня неправильно понял. Просто столь ультимативный пост - выносить, что только можно - это круто. Я как-то читал статью про программеров на джаве, которые из программы "Hello world" сделали абстрактную фабрику, стратегию, фасад и еще кучу всего. Приложение получилось очень расширяемым, но нафиг никому не нужным.
    Твой конкретный пример - ок. Бонды считать - не пива выпить. Но есть куча случаев - когда действия над сущностью очень простые и выносить персистность в отдельный слой - можно, и это правильно. Но, не нужно. Генерится до фига классов, которые нафиг не нужны. Мой посыл таков - не надо выключать голову. Лучшее - враг хорошего. Ну судя по тому, как ты предлагаешь использовать интерфейсы - не мне тебе об этом говорить :). Критерий простой - сложный класс - разбивай его до тех пор - пока он становится простым в понимании. Выделять из класса какую-то часть функционала только из-за того, что она должна быть выделена ПО ПРАВИЛУ. Но не надо быть рабом догм!!! Я знаю, что я говорю ересь с точки зрения религии паттернов и ООП, но каждый сам определяет на сколько он готов слепо верить!!!

    ОтветитьУдалить
  8. Жень, смотри, мне казалось, что этот пост как раз таки дает четкий критерий, когда можно положить на все принципы и паттерны - это сложность.

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

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

    Так что, в этом плане, we're on the same page, как говорят наши заокеанские друзья;)

    ОтветитьУдалить
  9. Всегда, когда мне говорят, что не надо усложнять, что, мол, так можно дойти до абсурда и в программе "Hello world!" заставить своих разработчиков делить приложение по слоям я отвечаю: "Мы не пишем простых приложений.". А те кто этим занимается не читает подобных статей.

    А еще у меня есть хороший товарищ, который сыпет летучими фразами. Часто когда ему рекомендуют предусмотреть в ПО ту или иную возможность для расширения он всегда отвечает: "Такого никогда не будет!". Бывало он потом ночами просиживал пытаясь заставить работать свой говнокод, когда ТАКОЕ все-таки случалось.
    Я теперь когда реализую какой-либо паттерн, который делает класс закрытым, но оставляет возможности его расширения всегда думаю: "Такого никогда не будет! Но все-таки..." :-).

    ОтветитьУдалить
  10. @Serg Boskitov: да, тут сложная грань между изменениями, которые реально могут произойти и к ним действительно нужно быть готовыми, и изменениями, вероятность которых минимальна.

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

    ОтветитьУдалить
  11. Сергей, а можно ли пояснить, что значит количество исходящих связей (efferent coupling)?

    Что такое количество входящих связей (afferent coupling) мне понятно, это вроде как количество связей(классов, интерфейсов) от которых зависит наш класс.
    А вот что значит второе понятие не могу взять в толк.

    ОтветитьУдалить
    Ответы
    1. Скорее всего наоборот. Количество исходящих связей - это количество классов от которых зависит наш класс, а количество входящих классов - это количество классов, которые зависят от нашего класса. Если конечно я правильно все понял.

      Удалить
  12. Вопрос по поводу формулы в разделе "Метрика стабильности класса". Под ней комментарий:
    "Из этой формулы вытекает два следствия: (1) чем больше у класса зависимостей, тем менее стабильным он является и (2) чем большим количеством классов он используется, тем больше вреда может быть при его изменении."
    1 - понятно. А по 2 - интуитивно согласен с таким выводом, но как это следует из формулы? Получается ведь чем больше у класса пользователей (Ca) тем I - меньше, а значит стабильнее. Вот этот момент не очень понял. Значит стабильность класса не определяется исключительно тем что в нем самом есть (бизнес логика/внешние зависимости), а зависит еще от того сколько у класса есть пользователей? Тоесть если моей библиотекой пользуется весь мир (милионы пользователей) то I стремится и 0 и она автоматически становится, грубо говоря, абсолютно стабильной?

    ОтветитьУдалить