Comments 41
Например, в C может быть какая-то функция, которая должна вести себя немного иначе, если её вызывает А, чем если её вызывает B.
Например, разработчик A может не понимать какой-либо параметр для функции C, поскольку он относится только к системам E и F.Если мы начинаем вносить правки в функционал блока C в зависимости от «хотелок» зависимых систем — уже что-то пошло не так! Тем более, если эти правки специфичны.
А проблемы неактуальности документации, потери автономности, поддержки качества, версионирования присущи практически всем более-менее крупным проектам. Они не исчезнут при отсутствии повторного использования.
Например, в C может быть какая-то функция, которая должна вести себя немного иначе, если её вызывает А, чем если её вызывает B.
Вместо расширения функционала С можно вынести специфичное поведение в А.
Проблема только в том, что от С тогда может вообще ничего не остаться.
Иначе можно преждевременно оптимизировать до бесконечности.
Кривая развития каждого проекта петляет по-своему, и будущее не предопределено, поэтому стоя на берегу сложно выбрать между «пилим всё монолитно в А» и «сразу раскидываем функционал в А и С, вдруг пригодится».
Выбрав первый вариант можно сесть в лужу, когда появится В (но это если появится), выбрав второй — можно убить втрое больше времени на создание подсистем А и С, упустить клиентов, а потом ещё окажется, что В и не будет никогда, или будет, но работать с А и С в текущем варианте не сможет.
Ну и пока делаем «чтобы просто работало» — вполне можно делать быстро и как получится, а когда стабилизированы хотелки, апи и прочее — тогда начать делать новую версию, с нуля, с нужными разделениями и вероятно на более подходящем языке. Не затягивать этот процесс, а то легаси с детским кодом потом много лет будет аукаться.
Если мы начинаем вносить правки в функционал блока C в зависимости от «хотелок» зависимых систем — уже что-то пошло не так! Тем более, если эти правки специфичны.
Дык вот реальность во многих коммандах именно такая. Плохо или с недостатком опыта обдуманное (см. ссылку на «Что видишь, то и есть») выделение "общего кода"…
Причем на моей памяти именно аргумент DRY, который не так легко паррировать в тех же самых кругах ;)
Мы пишем макарона-стайл код, поэтому повторное использование не работает, понятненько?
Имхо, это один из вариантов "преждевременная оптимизация — корень всех зол".
Далеко не всегда однозначно можно сказать, что C нужно выделять. Кроме того, нужно понимать, что выделение C — это не столько про скорость разработки (это надо еще подумать, что быстрее — копипаста или унести кусок функционала в отдельный класс/модуль), а про стоимость поддержки.
Соответственно, вариант "пишем в лоб два куска со сходным функционалом, не гнушаясь копипастой — проверяем боем — оптимизруем/реорганизуем" вполне себе годный вариант, который благодаря фазе "проверяем боем" отлично покажет есть ли смысл выносить C, да и вообще, есть ли это общее C.
Разумеется, это не касается совсем уж очевидных случаев, когда декомпозиция понятна заранее.
Если вы пишите отдельное приложение, но не библиотеку, пакет модуль, плагин или фреймвок, то повторное использование кода возможно только на уровне самого приложения.
Наконец, хотелось бы узнать ваше мнение или опыт крупномасштабного повторного использования. Когда оно работает, а когда терпит неудачу?
Есть мнение, что фреймворк-мейкерам не нужно отчаиваться в стремлении изготовить более высокоуровневое средство для задания логики С. Ну, то есть, когда логику C будет быстрее переписать заново со слов пользователя, чем кодить программистским языком с привлечением программистских же библиотек.
Опыт тоже есть в этом.
Полная лажа. Выделение и переиспользование знаний, технологий, решений, деталей, чего угодно — основа прогресса.
Стройте все изначально из правильных частей и не будет проблем с выделением.
Повторное — это когда С берется из системы А, интегрируется в систему В и дальше системы А и В идут параллельными непересекающиеся курсами. И такой вариант даёт выигрыш для системы В по сравнению с "а давайте напишем С заново". Обычно под С заводится свой репозиторий, где для каждой системы(А, В и т.д.) заводится своя ветка С которая используется только в одной системе и нигде больше. А в транк сливают результаты фиксов в ветках и он используется только для ветвления при появлении нового проекта которому нужно С.
Автор оригинальной статьи демонстрирует главный антипаттерн разработки модульных приложений: представление сложности обеспечения модульности как сущностной (связанной со сложностью задачи) вместо акцидентной (связанной с существующими инструментами и практиками.
Все аргументы сводятся к одному: у нас говнокод и поэтому повторное использование "очень сложно".
У меня есть успешный опыт превращения спагетти-монолита в свободно расширяемый набор модулей за счет правильной методики и созданного в соответствии с ней велосипеда.
Принципы модульности:
- Автономность — модули можно создавать, тестировать и эксплуатировать независимо от любого конкретного набора других модулей. Это сразу требует, чтобы все зависимости и вся реализованная функциональность модулей описывались явными контрактами на уровне кода.
- Компонуемость — сборка приложения из набора модулей должна быть простой задачей, в идеале — решаемой автоматически.
- Рекурсивность — из набора модулей должен легко собираться композитный модуль, неотличимый от обычных модулей снаружи.
- Прозрачность — должна иметься возможность сгенерировать актуальную диаграмму компонентов для модулей в работающем приложении.
- Ортогональность — инструмент для организации модульности должен быть библиотекой, а не фреймворком. Правки в коде для поддержки модульности должны быть минимально возможными.
Для самого кода (к примеру, ASP.NET MVC + React), для композиции модулей (DI?)?
Кстати, а модуль — это что? Библиотека?
А можете написать, какие технологии использовали?
Технологии тут как раз нерелевантны из-за пункта 5.
- Изначально делалось на Delhi 2007
- Контрактами были COM-интерфейсы (только из-за наличия у них GUID)
- Модуль — объект, реализующий интерфейс IModule: GUID модуля, имя модуля, список реализованных интерфейсов, список интерфейсов-зависимостей, методы для инициализации и финализации.
- Из набора модулей автоматически строился граф зависимостей.
- Из графа автоматически генерировалась диаграмма компонентов на языке PlantUML.
- Порядок инициализации и наличие циклов определялось топологической сортировкой.
- Композитный модуль строился на основе графа зависимостей подмодулей. Его зависимости — все интерфейсы, которые подмодули не реализовали сами, а реализованные контракты — подмножество таковых у внутренних модулей.
Сейчас в свободное от основной работы время занимаюсь библиотекой для дотнета: https://github.com/Kirill-Maurin/FluentHelium
Доклад по теме (прошлый февраль): https://www.youtube.com/watch?v=Gd9Ze7-CIb0&t=1005s
DI-контейнеры как есть для модульности пригодны только ограниченно: с прозрачностью и рекурсивностью у них никак.
Надо бы найти время и сделать-таки серию постов.
Кажется что ваш интерфейс IModule нарушает ваше же требование ортогональности
Интерфейс — не нарушает.
Ортогональность соблюдена — и внутри, и снаружи модуля может быть что угодно, в отличие от требований фреймворков.
От COM взяли только базовые интерфейсы с GUID и счетчиками ссылок: Delphi в них умеет из коробки.
Реестр и прочее ActiveX барахло не использовалось.
Интерфейс же не мешает как реализовать модуль средствами на свой выбор, так и встроить реализованное в уже имеющийся проект.
Пример: двусторонняя интеграция с Autofac, можно как сделать модуль из контейнера, так и зарегистрировать модуль в вышестоящем контейнере
github.com/Kirill-Maurin/FluentHelium/blob/master/FluentHelium.Autofac/AutofacModuleExtensions.cs
То есть сделать ту работу, которую обычно неявно делает Autofac…
Autofac, как и другие DI-контейнеры, эту работу вообще не делает ни явно, ни неявно. Нет метода, который вернул бы исчерпывающий список неразрешенных зависимостей. Только рантайм, только хардкор, только при попытке резолва из заранее сконфигурированного контейнера.
Он их определяет, только не собирает в один список.
Этого он тоже не делает.
Максимум, что умеет Autofac: при попытке разрешения найти первую неразрешаемую зависимость или цикл, после чего выкинуть исключение или вернуть false для TryResolve().
Эта работа годится только для минимальной индикации ошибок конфигурации контейнера.
Для задач модульности это и слишком поздно, и слишком мало.
Так определяется непрозрачность и нерекурсивность контейнеров:
- Граф зависимостей неявный
- Автоматическое определение зависимостей работает для типов, но не для контейнеров.
- До попытки разрешения не работает практически ничего.
С моей библиотекой можно построить явный граф зависимостей, определить циклы и неразрешенные зависимости еще до инициализации самих модулей.
Отсюда и велосипедостроение: я, к сожалению, не видел ни одной библиотеки, которая умела бы это из коробки.
- Нет никакой необходимости разрешать зависимости до тех пор, пока не начата инициализация модулей.
- Список неразрешенных зависимостей для графа модулей необходимо построить заранее: это позволяет разрешить их за счет ядра или заявить как зависимости композитного модуля.
Autofac ничего из этого не умеет, превращая разрешение зависимостей в игру "Сапер" периода выполнения без индикации числа соседних мин.
Очевидно, что внутри Autofac есть код который эти зависимости определяет
Очевидно, что никакого списка зависимостей для дексриптора модуля Autofac создать не может, следовательно ваш тезис из цитаты ниже неверен.
Вот только чтобы сделать модуль из контейнера — вам нужно сначала вручную сформировать для него дескриптор, для чего нужно найти все зависимости. То есть сделать ту работу, которую обычно неявно делает Autofac…
Это очевидно неверно «снаружи».
Под капотом Autofac тоже ничего подобного не делает, по крайней мере на момент моего копания в его исходниках.
В нем нет выделенной функциональности построения графа зависимостей. Нет и анализа с целью определения исчерпывающего списка неразрешаемого в рамках имеющихся регистраций.
Autofac только пытается разрешать зависимости на лету, неразрешаемое он специально не ищет.
Для того, чтобы превратить контейнер в модуль вашего формата, разработчик должен найти все зависимости и перечислить их.
Autofac тоже внутри ищет зависимости и разрешает их в правильном порядке. Но вторая задача намного проще первой.
И если я сам найду и определю все зависимости — мне уже не нужен Autofac, вот в чем проблема.
И если я сам найду и определю все зависимости — мне уже не нужен Autofac, вот в чем проблема.
Нет такой проблемы.
- То, что надо реализовать определяется не контейнером, а постановкой задачи.
- То, что надо выставить как зависимости — это НЕ список всех зависимостей в контейнере, это список только тех из них, которые не разрешаются внутри самого контейнера.
- Список таких зависимостей также определяется при постановке задачи — проектируя модуль, вы решаете, что он НЕ должен обеспечивать сам.
- Если уровнем выше использовать композитный модуль, то он сможет определить неразрешенные зависимости подмодулей автоматически и выставить их как свои — тут контейнер действительно может быть лишним.
- За связи, невидимые снаружи модуля, контейнер отвечает по-прежнему и нужен внутри модуля именно с
этой целью. - Простой для реализации модуль, конечно же, проще сделать без контейнера.
- В случае использования модуля, снаружи у вас может быть один контейнер, внутри другой, но оба они друг о друге ничего не знают и никак друг от друга не зависят.
- Это показатель реальной автономности модулей
Дубль, просьба удалить
Реальность повторного использования