Pull to refresh

Comments 19

auto* unitOfWork1 = container.Resolve<UnitOfWorkDescriptor>();
unitOfWork1->CrateEmployee("Bob", /* departmentId: */ 1);

std::cout << "Create unitOfWork2...\n";

auto* unitOfWork2 = container.Resolve<UnitOfWorkDescriptor>();
unitOfWork1->CrateEmployee("Alice", /* departmentId: */ 2);

У вас тут ошибка

И на мой взгляд, интерфейсами это было решить куда удобнее

Большое спасибо, исправил.

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

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

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

Странная какая то статья. "Вроде бы обо всем и не о чем"

как обуздать зависимости

Статья достаточно интересная, для общего развития, но человеку не в теме, чтобы увлечься, лучше "один разу пощупать, чем сто раз увидеть", т.е., лицезреть конкретный пример на уровне GUI, реализованного с помощью C++ / WTL или WinAPI. Тогда, даже не вникая в детали, сразу видны достоинства и недостатки, "блеск и нищета" и все такое.


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


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


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

Большое спасибо за развёрнутый комментарий, многие мысли резонируют с моими собственными.

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

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

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

Правы классики: «Практика – критерий Истины!». Мы можем сколько угодно строить теоретических конструкций, но если они, при соприкосновении с действительностью, начинают «сыпаться», то возникает резонный вопрос, а всё ли ладно в «Датском Королевстве»?


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


Для примера. Начал я писать простую программу для распознавания встроенных субтитров в видео-роликах. Там всего три окна. Файловая панель, для выбора медиа-файла, окно с видео (на базе FFPlay.c из опенсорсного пакета FFMpeg) и окно с распознанным текстом субтитров. В принципе программа была почти готова, даже научился распознавать субтитры из однотипных ютубовских роликов, типа «EasyFrench». Но потом проект забросил по двум причинам. Первая, лень было делать настройку на разные шрифты из нетиповых видео и, вторая, более важная, смысла в этом особого не было. Поскольку все эти данные я собирал для своей обучающей программы «Сколиум», а там основная концепция это «запоминание руками». Поэтому, какой резон тратить силы на автоматическое распознавание, чтобы потом набирать этот текст вручную для целей его лучшего усвоения? Ведь, можно и сразу набрать его вручную, не так ли? Чем чаще будешь набирать иностранные фразы, тем скорее их запомнишь.


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


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


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


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


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

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

После 6 лет сопровождения проекта с использованием Dependency Injection скажу только одно - прежде чем вводить их в проект ПОДУМАЙТЕ и откажитесь!

Опишите подробнее ваш не удачный опыт использования:

  1. с++ или язык с рефлексией

  2. рантайм/позднее связывание/интерфейсы или как здесь шаблоны/дженерики/компилтайм

  3. Описание самих зависимостей обрабатывается в рантайме или как здесь в компил тайме

  4. Какая именно боль у вас от ДИ

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

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

А ещё вам не надо на каждую зависимость писать интерфейс и еслииу вас моки то при каждом изменение интерфейса вам не надо делать его 3 раза, а ещё в дебаге вы увидите настоящие типы ВСЕХ зависимостей сразу и одновременно, а не какой-нибудь пимпл который пока не пробьешься сквозь стэк не узнаешь и то по одной штуке за раз. Ну и само собой без виртуальных вызовов конечно же сильно быстрее, но это уже как раз вишенка, а не сам торт.

Итак по порядку:

  1. Java + Guice

  2. Runtime связывание, интерфейсы, шаблоны и все как принято

  3. Само связывание осуществляется в runtime, но все поставляемые зависимости описываются в статике. Слава богу, хотя бы без XML.

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

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

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

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

Это легкое описание той головной боли, которая получена благодаря Dependency Injection. И да, DI никак не помогает упростить написание логики приложения.

PS имел счастье почитать код, до внедрения механизма DI... читал код и плакал - все понятно, логично и надёжно. Нет, вот так - НАДЕЖНО!

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


Конечно, такой подход уберет часть головной боли.
Но фокус состоит в том, что если бы не DI - то у меня вообще не было бы головной боли.

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

Спасибо за статью и за этот коментарий.

Тоже страдал !).

.net framework 4.5 , рефлексия , шарились интерфейсы на тысячи классов ,тестирование IRepository - боль.

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

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

Когда, например, в конструкторе класса явно перечислены все зависимости, этого вполне достаточно, а конкретная реализация, чаще всего, может быть подставлена на старте приложения. Таким образом мы получаем так называемый self-describing код и безо всяких дополнительных усилий, позволяя программисту быстрым взглядом на конструктор понять, от чего этот класс зависит.

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

Отличная статья, сам пользуюсь этим подходом. Автор и целый ряд комментаторов правильно заметили:

  • это отдельный стиль и к нему следует привыкнуть

  • да, отладка на этапе компиляции не тривиальна, но действительно, после успешной сборки, тебя не ждут неожиданности

  • код получается шустрым, конкретно мне для криптографической библиотеки это сильно помогает. Пример такой открытой библиотеки: Botan, моя проприетарная поэтому смысла ссылаться на нее здесь нет. Но сам подсматриваю идею в Botan, в том числе :-)

  • ну и ещё один из минусов, это большая библиотека и да, компилятору может не хватить памяти :-) особенно 32-х разрядной версии vc++

Автору спасибо за систематизацию и детальное пояснение.

Увы, у шаблонного решения для DI до прихода концептов и модулей есть несколько проблем:

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

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

Но до С++20 использовать решение из статьи, думаю, на практике достаточно больно

Согласен с обоими пунктами, пришёл к тем же выводам, пока делал библиотеку.
Но решить проблему отсутствия концептов частично всё-таки можно.

Для этого нужно объявить члены шаблонного аргумента внутри соответствующего ему типа-дескриптра.

struct UnitOfWorkDescriptor {
    int AddDepartment(const std::string& departmentName);
    int AddEmployee(int departmentId, const std::string& employeeName);
    void RemoveDepartment(int departmentId);
    void RemoveEmployee(int departmentId, int employeeId);
    ...
};

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

Sign up to leave a comment.

Articles