Эволюция управления зависимостями в коде

27 июня 2013 г.

На последней конференции .NET разработчиков, которая прошла 28 апреля в Челябинске, я проводил мастер-класс по управлению зависимостями в коде. Моей целью было показать, как эволюционировал подход к управлению на протяжении времени и какие идеи актуальны в данный момент.

Несколько лет назад я записывал видео на эту же тему:

На мастер-классе и в этом видео показываю шаги, по которым идет эволюция:

  1. Проблемы в коде, нарушения принципов проектирования, бесконечный Code&Fix
  2. Попытка применить принцип IoC и написание простейшего ServiceLocator
  3. Переход к Dependency Injection
  4. Углубленное использование возможностей DI фреймворков, таких как: настройка жизненного цикла, создание объектов через фабрики, различные способы инжектирования, полностью абстрактные фабрики и т.д.

За время своей работы я прошел все эти этапы. Расскажите как вы начинали работать с управлением зависимостями в коде? Какие практики вы считаете на данный момент самыми актуальными по этой теме?

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

  1. Спасибо за мастер-класс. ИМХМО применять все эти техники, как и всё, надо с умом, ибо порой они приводят только к усложнению кода. В свое время когда выбирали IoC контейнер, открытием для меня было насколько Ninject оказался медленным http://www.iocbattle.com/

    ОтветитьУдалить
  2. Павел, буквально недавно в скайпе обсужали одну проблему с over-design. Хочется везде много интерфейсов и всё инжектиться и всё невероятно гибко :) Согласен, что всё в меру, но этап с экспериментами должен у каждого пройти.

    ОтветитьУдалить
  3. Александр, я прошел те же этапы что и вы. Только Service я называю DataSource, а Repository - DataProvider - ом. Но это мелочи. В целом все так, но все таки я хотел бы немного подискутировать. Из ваших картинок с архитектурой можно подумать что у вас два слоя.
    Application c интерфейсами к сервисам на одном слое и реализация сервисов с интерфейсами к репозиториям и реализацией этих репозиториев на другом. На самом деле слоя три:

    1) Слой приложения. Здесь лежать Application c интерфейсами к сервисам.

    2) Слой бизнес логики. Сервисы с интерфейсами к репозиториям.
    3) Слой доступа к данным. Реализации репозиториев.

    Слой 3 знает о существовании слоя 2, слой 2 знает о существовании слоя 1. Из этого вытекает, что слой 1 не знает ничего о слое 3 и ситуации, что в слое приложения кто то попросит у сервис локатора реализацию репозитория, просто не может быть.
    Еще соображения по поводу слоя абстракции над IoC контейнером. Я тоже очень люблю создавать слои абстракции над всем и сейчас пользуюсь собственной абстракцией над контейнерами. Но это все таки велосипед. Сейчас в .net существует ServiceLocator, доступный в нюгет, представляющий собой уже готовую абстракцию над любым IoC контейнером и делать свой велосипед как бы больше и не нужно.(Аналогично сборке Common.Logging являющейся уже готовой абстракцией над любым логгером).

    ОтветитьУдалить
  4. Спасибо, что обратили на это внимание. Пример конечно не отражает никакую реальную архитектуру и сделан только для понимания.

    На счет слоев, их может быть и 3 и 10, я стараюсь не думать об этих слоях, создаю их по мере надобности.

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

    > Сейчас в .net существует ServiceLocator, доступный в нюгет

    Ну это же просто прекрасно, значит все идет к лучшему :)

    ОтветитьУдалить
  5. Еще я бы обратил внимание на то, что хорошим стилем программирования является изоляция IoC контейнера в самом верхнем слое (слое приложения), где, в идеале, и должно происходить инстанцирование всех зависимостей. Использование IoC контейнеров в репозиториях и сервисах напрямую должно быть запрещено, и, если уж есть необходимость инстанцироваться именно там, то нужно явно передавать фабрику, скрывающую IoC контейнер. Еще вы ясно недооцениваете конфигурацию через xml, но это незаменимый инструмент при работе над большим проектом. Огромный плюс в том, что для билда всего вашего проекта, если у вас IoC контейнер конфигурируется только через xml, вам достаточно в референсы включить только интерфейсы сервисов и ни одной реализации, кроме самого IoC контейнера. И, если мой проект разделен на 10 сервисов, а сейчас я работаю только над одним, я перестаю зависеть от реализации других сервисов в приложении, просто выкинув их из xml конфига. В случае же указания зависимостей вручную этого сделать невозможно. Я для себя вывел простое эмпирическое правило, если я работаю над проектом состоящим только из одной сборки, то заводить зависимости в IoC лучше через кодирование, если же проект разбит на несколько библиотек, то только xml.

    ОтветитьУдалить
  6. > изоляция IoC контейнера в самом верхнем слое

    Это в случае, если использовать инъекцию зависимостей. Вполне допустимо использовать ServiceLocator.

    > Еще вы ясно недооцениваете конфигурацию через xml

    Кто вам сказал? :) В своих проектах мы активно используем этот способ конфигурирования.

    ОтветитьУдалить
  7. "Хочется везде много интерфейсов и всё инжектитЬся" -


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

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

    зы) tsya.ru (кстати, именно с этого сайта путем "нескольких рукопожатий" я и попал на ваш блог)

    ОтветитьУдалить
  8. "Только Service я называю DataSource, а Repository - DataProvider - ом. Но это мелочи. " - не такие уж мелочи, в свете современных "баззвордов".

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

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

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

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

    каким бы сложным и дорогим оказался бы автомобиль с такой сомнительной возможностью. Но автопроизводители умнее программистов и так не делают. потому что понимают, что в 99.99 % случаев жизненный цикл двигателя совпадет с жизненным циклом автомобиля.

    ОтветитьУдалить
  9. > когда начинаешь понимать, что же за всем этим скрыто, то приходишь к тому, что
    (применительно к дата-лейеру) скрыта-то старая добрая модель доступа к данным через адаптер.

    Если так рассуждать, то со времен программирования на перфокартах тоже не многое изменилось. Те же операции сложения и умножения.

    > каким бы сложным и дорогим оказался бы автомобиль с такой сомнительной возможностью

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

    В это же время никто не станет вас просить после постройки дома вставить в центр пару этажей или уменьшить кол-во подъездов.

    > типа через "ынтерфейсы" подключать разные сервисы (раньше было - разные провайдеры), что самое смешное, в реальной жизни не используется ни в одном из живых проектов

    Это поверхностное понимание DIP. Основная цель - минимизировать цену внесения изменений в систему за счет низкой связности. Дополнительный плюс - тестируемость системы модульными тестами.

    А подмена сервисов - это как одна из возможностей, которая не так часто используется.

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



    Я тоже когда то так думал. Но это проходит с опытом. Раньше, я как и вы считал что не стоит городить интерфейс к тому, что не подразумевает нескольких различных реализаций. Но это заблуждение. Реализаций ВСЕГДА как минимум две. Первая - это ваш рабочий сервис, вторая - это moq или stub для тестов. Если вы написали сервис и не соизволили приделать к нему интерфейс, вы лишаете своих коллег возможности писать и тестировать код, связанный с вашим сервисом, в отрыве от функциональности вашего сервиса.

    ОтветитьУдалить
  11. >"Только Service я называю DataSource, а Repository - DataProvider - ом. Но это мелочи. " - не такие уж мелочи, в свете современных "баззвордов".

    Название Provider у нас не случайное. Когда мы согласовывали именования основных модулей проекта, мы хотели подчеркнуть что провайдер имеет монопольный доступ на данные, возвращаемые им. В нашей архитектуре за масштабирование и кеширование отвечат самый нижний слой - слой доступа к данным. И провайдер скрывает все сложности хранения в себе. То есть, если UserProvider хранит данные по пользователям, то вы никоим иным образом не сможете получить имя пользователя, кроме как вызвав метод IUserProvider.GetUserName. Сервисам, занимающимся бизнес логикой, ни к чему знать что для хранения информации по пользователям мы используем две БД, при этом кешируя в Reddis часть информации. Только и всего.
    Репозиторий же более общий паттерн, часто это просто слой абстракций над множеством хранимых процедур.
    Слово Service в нашем проекте было зарезервировано под другой тип объектов и чтобы не возводить путаницы мы решили использовать новое именование.

    ОтветитьУдалить
  12. > Сравнения разработки ПО с производством материальных продуктов некорректны. ПО постоянно меняется, в течение месяца продукт может полностью измениться. После релиза мы можем целиком переделывать и заменять многие части.

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

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

    >> В это же время никто не станет вас просить после постройки дома вставить в центр пару этажей или уменьшить кол-во подъездов.

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

    > Это поверхностное понимание DIP. Основная цель - минимизировать цену внесения изменений в систему за счет низкой связности. Дополнительный плюс - тестируемость системы модульными тестами.

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

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

    впрочем, опять же на память приходит слово "прокси", означавшее в большинстве случаев то же самое, но сейчас модно рассматривать систему с точки зрения зависимостей)

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

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

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

Моя книга «Антихрупкость в IT»

Как достигать результатов в IT-проектах в условиях неопределённости. Подробнее...