Несколько аргументов против Dependency Injection и Inversion of Control

Помнится, во времена .NET 1.1 и 2.0 можно было часто увидеть пророчества майкрософтовских евангелистов, мол, скоро любая домохозяйка сможет создавать сайты и писать программы. Большинство программистов посмеялось, но, как выяснилось, кто-то отнёсся к этому серьёзно. Во всяком случае, это объясняет, почему шаблоны проектирования IoC/DI получили второе дыхание в 2000-х, причём даже внутри самой MS (желаю Вам никогда в жизни не столкнуться с SCSF).
d77fada98f344babaff9c89522607514.jpg
С точки зрения теории разработки ПО лично мне гораздо чаще приходилось читать или слышать хвалебные статьи и отзывы об IoC/DI, но, как всегда, критика тоже есть. Можно ознакомиться, например, здесь (англ.), здесь (англ.), тут (хабр), ещё (англ.). В частности в вину ставится нарушение принципа инкапсуляции в ООП.

Но мне хотелось бы не вдаваться в теологические диспуты (в чём я не считаю себя экспертом), а остановится на трудовых буднях (которые мало, на мой взгляд, освещаются в публикациях).
Действительно, много ли Вы встречали книг или статей по программированию, где указывалось бы на то, что код всегда содержит ошибки (даже калькулятор невозможно покрыть 100% тестированием) и нужно в него вставлять возможности диагностирования ошибок в продуктивной среде, где Вам никто не даст поставить Studio и отладиться? Что если продукт окажется достойным и найдёт своего пользователя, то он обязательно будет дорабатываться, но делать это, вполне вероятно, будут другие люди, с не известно каким уровнем подготовки? Вот я ни одной не могу вспомнить.

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

Итак.

Сложность для понимания


Где-то читал в своё время — то ли у Брукса, то ли у Листера с ДеМарко, точно не помню, — что языки программирования придуманы не для машин, а для людей. Машине, в конце концов, без разницы, забьёте ли вы руками в файл нолики и единички или сначала напишите текстовые команды, а затем откомпилируете в исполняемый код. Компилятору всё равно, будет ли программист вставлять комментарии в свой код или посчитает, что тот является самодокументированным. Интерпретатор JavaScript одинаково обработает обфусцированный, сжатый скрипт и отформатированный, с человекопонятными названиями переменных и функций.

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

Если ваша программа состоит из десятка-другого объектов (т.е. это не более 100 файлов с описанием классов и интерфейсов), то воспринять всё это вместе в виде единого целого относительно просто. Мне в своё время довелось сопровождать настольное приложение, созданное на основе Microsoft Smart Client Software Factory (потом ему на смену MS запустила Prism, но уверен, что IoC/DI там так же плотно задействованы), по своему функционалу не такое уж сложное, но состоявшее из пары десятков проектов (в терминах Visual Studio), а это сотни и сотни классов, отвечающих и за DAL, и за логику, и за пользовательский интерфейс, и за событийную модель под ним. Каждый раз, когда на горизонте появлялась задача по добавлению новой фичи, меня начинало слегка колотить изнутри, т.к. всплывала перспектива увлекательно провести несколько дней, чтобы догадаться, куда нужно «воткнуть» обработку нового поля объекта из БД, точнее — по каким классам распиханы зависимости. При слабой связанности классов, поверьте, это не самая тривиальная задача.

Возможно, мой мозг начал костенеть и стал менее восприимчив к новым идеям (хотя IoC/DI были придуманы, кажется, в начале 90-х), но мне сложно понять, чем стал неугоден принцип инкапсуляции из ООП.

Малоинформативные отладочные данные


Вспоминается цитата с башорга:
#define TRUE FALSE //счастливой отладки, уроды (*)

(*) фраза была несколько смягчена, дабы не навлекать на Ресурс.

Смешно, не правда ли? А вот такие шутки штуки я встречал в своём проекте на этапе запуска в ПЭ (и было мне не совсем смешно):

Stack Trace
StructureMap.StructureMapException: StructureMap Exception Code: 202
No Default Instance defined for PluginFamily System.Func`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
at StructureMap.BuildSession.<.ctor>b__0(Type t)
at StructureMap.Util.Cache`2.get_Item (KEY key)
at StructureMap.BuildSession.CreateInstance (Type pluginType)
at StructureMap.Pipeline.Instance.createRawObject (Type pluginType, BuildSession session)
at StructureMap.Pipeline.Instance.Build (Type pluginType, BuildSession session)
at StructureMap.Pipeline.ConstructorInstance.Get[T](String propertyName, BuildSession session)
at lambda_method (Closure, IArguments)
at StructureMap.Construction.BuilderCompiler.FuncCompiler`1.<>c__DisplayClass2.b__0(IArguments args)
at StructureMap.Construction.InstanceBuilder.BuildInstance (IArguments args)
at StructureMap.Pipeline.ConstructorInstance.Build (Type pluginType, BuildSession session, IInstanceBuilder builder)
at StructureMap.Pipeline.SmartInstance`1.build (Type pluginType, BuildSession session)
at StructureMap.Pipeline.Instance.createRawObject (Type pluginType, BuildSession session)
at StructureMap.Pipeline.Instance.Build (Type pluginType, BuildSession session)
at StructureMap.Pipeline.ObjectBuilder.Resolve (Type pluginType, Instance instance, BuildSession session)
at StructureMap.BuildSession.CreateInstance (Type pluginType, Instance instance)
at StructureMap.BuildSession.CreateInstance (Type pluginType)
at StructureMap.Pipeline.Instance.createRawObject (Type pluginType, BuildSession session)
at StructureMap.Pipeline.Instance.Build (Type pluginType, BuildSession session)
at StructureMap.Pipeline.ConstructorInstance.Get[T](String propertyName, BuildSession session)
at lambda_method (Closure, IArguments)
at StructureMap.Construction.BuilderCompiler.FuncCompiler`1.<>c__DisplayClass2.b__0(IArguments args)
at StructureMap.Construction.InstanceBuilder.BuildInstance (IArguments args)
at StructureMap.Pipeline.ConstructorInstance.Build (Type pluginType, BuildSession session, IInstanceBuilder builder)
at StructureMap.Pipeline.SmartInstance`1.build (Type pluginType, BuildSession session)
at StructureMap.Pipeline.Instance.createRawObject (Type pluginType, BuildSession session)
at StructureMap.Pipeline.Instance.Build (Type pluginType, BuildSession session)
at StructureMap.Pipeline.ObjectBuilder.Resolve (Type pluginType, Instance instance, BuildSession session)
at StructureMap.BuildSession.CreateInstance (Type pluginType, Instance instance)
at StructureMap.BuildSession.CreateInstance (Type pluginType)
at StructureMap.Pipeline.Instance.createRawObject (Type pluginType, BuildSession session)
at StructureMap.Pipeline.Instance.Build (Type pluginType, BuildSession session)
at StructureMap.Pipeline.ConstructorInstance.Get[T](String propertyName, BuildSession session)
at lambda_method (Closure, IArguments)
at StructureMap.Construction.BuilderCompiler.FuncCompiler`1.<>c__DisplayClass2.b__0(IArguments args)
at StructureMap.Construction.InstanceBuilder.BuildInstance (IArguments args)
at StructureMap.Pipeline.ConstructorInstance.Build (Type pluginType, BuildSession session, IInstanceBuilder builder)
at StructureMap.Pipeline.SmartInstance`1.build (Type pluginType, BuildSession session)
at StructureMap.Pipeline.Instance.createRawObject (Type pluginType, BuildSession session)
at StructureMap.Pipeline.Instance.Build (Type pluginType, BuildSession session)
at StructureMap.Pipeline.ObjectBuilder.Resolve (Type pluginType, Instance instance, BuildSession session)
at StructureMap.BuildSession.CreateInstance (Type pluginType, Instance instance)
at StructureMap.BuildSession.CreateInstance (Type pluginType)
at StructureMap.Pipeline.Instance.createRawObject (Type pluginType, BuildSession session)
at StructureMap.Pipeline.Instance.Build (Type pluginType, BuildSession session)
at StructureMap.Pipeline.ConstructorInstance.Get[T](String propertyName, BuildSession session)
at lambda_method (Closure, IArguments)
at StructureMap.Construction.BuilderCompiler.FuncCompiler`1.<>c__DisplayClass2.b__0(IArguments args)
at StructureMap.Construction.InstanceBuilder.BuildInstance (IArguments args)
at StructureMap.Pipeline.ConstructorInstance.Build (Type pluginType, BuildSession session, IInstanceBuilder builder)
at StructureMap.Pipeline.SmartInstance`1.build (Type pluginType, BuildSession session)
at StructureMap.Pipeline.Instance.createRawObject (Type pluginType, BuildSession session)
at StructureMap.Pipeline.Instance.Build (Type pluginType, BuildSession session)
at StructureMap.Pipeline.ObjectBuilder.Resolve (Type pluginType, Instance instance, BuildSession session)
at StructureMap.BuildSession.CreateInstance (Type pluginType, Instance instance)
at StructureMap.Container.GetInstance[T](String instanceKey)
at NNN.BBB.Integration.Uvhd.Dispatcher.Start () in j:\.projects\DDD\trunk\NNN.BBB.UvhdIntegrationService\Dispatcher.cs: line 30

Очень информативно, согласитесь? BBB, DDD, NNN — это я намеренно изменил название проектов и пространства имён, которые указывали на наименование компании-субподрядчика. Но там ничего интересного для отладки не было. Dispatcher.Start () — это запуск службы MS Windows, точка входа в программу. StructureMap — это библиотека IoC. Ни единого упоминания какого-либо из бизнесовых методов, т.к. было сделано всё, чтобы исключить контекст из стека вызова.

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

Нивелирование достоинств компилируемых языков


Как говорится, беда не приходит в одиночку. Так и IoC/DI идут рука об руку вместе с шаблоном Service Locator, который тесно связан с идеей позднего связывания. При этом при компиляции решения не проверяется, соответствует ли сигнатура методов требованиям в точке вызова.
Так и случилось в моём случае из примера выше. В один из бизнесовых методов был добавлен новый параметр. Проект успешно скомпилировался, но отказался запускаться. Мне повезло.

Во-первых, в данном проекте было всего лишь около 50 классов и методом [научного] тыка удалось относительно быстро установить, что нужно ещё доработать класс загрузки конфигурации.

Во-вторых, исключение выбрасывалось в самом начале работы программы, и мимо этого было не пройти. К сожалению, не смог найти другой реальный пример и жизни из этого же проекта, т.к. сразу не отложил «в мемориз», а потом всё затерялось в килотоннах логов. Но инстанцирование обработчиков может производиться не сразу при старте приложения, а по мере необходимости — когда появятся пользовательские данные, требующие соответствующей обработки. Это может произойти спустя часы, дни и даже недели. Однако эффективность упомянутого выше метода поиска ошибок «было — стало» обратно пропорциональна времени, прошедшему с момента проблемного релиза, и количеству релизов, последовавших за ним.

Повышенные требования к уровню подготовки специалистов


Сами по себе сложность восприятия кода и трудности отладки не смертельны. Беда в том, что для целей внедрения, сопровождения и развития продукта нужны люди из плоти и крови. И очевидно, чем сложнее продукт, тем выше требования к уровню подготовки специалистов. А тут уже встают проблемы рынка труда и сферы образования, о которых, полагаю, не нужно никому тут рассказывать: грамотных, опытных спецов найти занимает много времени, содержать не дёшево, удержать и того сложнее. Да и на изучение «матчасти» уходят недели и даже месяцы.
Как следствие из данного аргумента хотел бы выделить ещё 3 уточнения.
  1. Я на своей практике не раз сталкивался с ситуацией, когда первоначальную команду разработчиков постепенно сменяла другая, с менее продвинутыми знаниями в теории программирования и передовых методах разработки софта. Просто потому, что после запуска продукта, бывает, на первый план выходит знание предметной области (например, законодательство, бизнес-процессы целевой аудитории, коммуникативные навыки), а не технологий кодирования. В такой ситуации чем сложнее код, тем быстрее продукт «эволюционирует» в замысловатое ожерелье костылей и заплаток.
  2. Чем сложнее код, тем дольше длится погружение в проект новичка.
  3. Не менее очевидно, что сужается круг участников команды, кто может внести свой вклад в код. Кто-то посчитает это даже плюсом. Но я сошлюсь даже не на свой опыт, а на книгу Рейнвотера «Как пасти котов. Наставление для программистов, руководящих другими программистами». Там приводится история ПМа и его программиста накануне дедлайна, когда обоих уволили из-за провала, хотя, скорее всего, они успели бы в срок, если бы руководитель помог своему программисту.

Заключение


В начале статьи были приведены ссылки на статьи, в некоторых из коих приводятся условия, когда можно применять DI/IoC, а когда этого лучше не делать. Я предлагаю дополнить список, когда НЕ надо использовать эти шаблоны, правилами, скорее, управленческого уровня, нежели из области компьютерных наук. Не используйте таковые шаблоны, если:
  • Вы не уверены на 100%, что на этапе запуска проекта, его стабилизации, опытной и даже промышленной эксплуатации будут оперативно доступны разработчики этого продукта.
  • Проектировщиком и разработчиком системы является один-единственный человек, который (не удивительно) не ведёт подробного документирования архитектуры своего детища.
  • У вас нет основания полагать, что на этапе проектирования Вы предусмотрели все варианты использования продукта и на этапе сдаче проекта или даже после его запуска не возникнет острой потребности в спешном порядке «допиливать» бизнес-логику; или если бизнес-среда, в которой предстоит плавать продукту, слишком изменчива, например, частые изменения регуляторов, конкуренция на рынке, отсутствие чётких стандартов/рекомендаций/практик в отрасли.

Комментарии (5)

  • 1 апреля 2017 в 11:12

    0

    Долго думал, в Управление проектами или в Разработку, ну пусть лежит здесь
  • 1 апреля 2017 в 12:10

    0

    Работаю с двумя реализациями IoC, Ни в одной не встречал ситуации «у нас все плохо, но мы не скажем почему», скорее всего по тому, что живых ситуаций потери зависимостей не так много и все они разжевываются новичкам буквально в первые недели работы. Гораздо сложнее объяснить про контекстно зависимость для части классов. Вот тут реально такие варианты решения «проблем» видел, что хоть вешайся. Тут остро стоит вопрос «я новичок и должен показать, что я умею круто программировать», в итоге — свои реализации стандартных контейнеров, обязательная «самая правильная» реализация синглтона (я уж не знаю, возможно это какое-то неформальное соревнование). А размер проекта вообще на сложность не влияет, если он изначально написан модульно.
  • 1 апреля 2017 в 12:17 (комментарий был изменён)

    +1

    Ну не знаю насчет сложности того же Prism, он сейчас в опенсорсе и заметно похудел. Но однажды мне пришлось полностью переписать DAL (заменить Refit на RestSharp) и я благодарил вселенную за то, что люди изобрели IoC, потому что вся процедура заняла буквально час-два. Даже не знаю, сколько пришлось бы ковыряться в противном случае.
  • 1 апреля 2017 в 13:02

    0

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

    под слабой связанности вы тут именно cohesion или coupling имеете ввиду?


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

    Интересно каким образом DI/IoC нарушает инкапсуляцию. Клиентский код как не знал о зависимостях используемых объектов так и не знает. Точно так же как объект который мы хотим получить не должен знать ничего о жизненном цикле своих зависимостей.


    Ну и IoC опять же способствует тому чтобы скрывать от объектов не интересующие их вещи. Сам принцип Don’t call us we call you об этом.


    И очевидно, чем сложнее продукт, тем выше требования к уровню подготовки специалистов.

    Может быть проблема как раз таки в низкой квалификации? Ну то есть решать надо именно эту проблему, а не симптомы устранять.


    Малоинформативные отладочные данные

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


    Так и IoC/DI идут рука об руку вместе с шаблоном Service Locator который тесно связан с идеей позднего связывания.

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


    Чем сложнее код, тем дольше длится погружение в проект новичка.

    причем тут IoC/DI? Если система спроектирована плохо, если абстракции используемые текут, если для того что бы разобраться в чем-то надо прошерстить всю систему… то у меня есть вопросы к подобного рода проектам.


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


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


    Но достаточно посадить его на часик-два и рассказать о проекте да что бы он почитал спеки/ассептанс тесты, и код начинает восприниматься уже вполне логично. И становится понятно почему штука по покупке валют лежит там, а трансфер валют там.


    Не используйте таковые шаблоны, если:

    Я так и не увидел предлагаемой альтернативы. Более того я так и не понял что плохого вы видите в IoC. Есть подозрение что под IoC вы имеете ввиду контейнер зависимостей, а не сам принцип.

    • 1 апреля 2017 в 13:27

      0

      +1 Особенно вот про это:

      >Так и IoC/DI идут рука об руку вместе с шаблоном Service Locator, который тесно связан с идеей позднего связывания.

      Потому что обычно как раз _не_ идут. DI как правило избавляет от надобности в Service Locator совсем.

© Habrahabr.ru