Pull to refresh

Comments 41

Мне кажется, что все описанные проблемы — это следствие неверного первого шага (выделение).
Например, в C может быть какая-то функция, которая должна вести себя немного иначе, если её вызывает А, чем если её вызывает B.
Например, разработчик A может не понимать какой-либо параметр для функции C, поскольку он относится только к системам E и F.
Если мы начинаем вносить правки в функционал блока C в зависимости от «хотелок» зависимых систем — уже что-то пошло не так! Тем более, если эти правки специфичны.

А проблемы неактуальности документации, потери автономности, поддержки качества, версионирования присущи практически всем более-менее крупным проектам. Они не исчезнут при отсутствии повторного использования.
Например, в C может быть какая-то функция, которая должна вести себя немного иначе, если её вызывает А, чем если её вызывает B.

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

Если от C ничего не остается после вынесения специфичного поведения в A и B — значит, никакого C никогда не существовало, и выделение его в отдельную библиотеку — одна большая ошибка.
UFO just landed and posted this here
Лично я считаю, что сначала надо дублировать, а уже потом, на этапе оптимизации, выносить дублирующийся код в отдельный модуль.
Иначе можно преждевременно оптимизировать до бесконечности.

Кривая развития каждого проекта петляет по-своему, и будущее не предопределено, поэтому стоя на берегу сложно выбрать между «пилим всё монолитно в А» и «сразу раскидываем функционал в А и С, вдруг пригодится».
Выбрав первый вариант можно сесть в лужу, когда появится В (но это если появится), выбрав второй — можно убить втрое больше времени на создание подсистем А и С, упустить клиентов, а потом ещё окажется, что В и не будет никогда, или будет, но работать с А и С в текущем варианте не сможет.
UFO just landed and posted this here
Обычно такой подход означает МНОГО напильника, гораздо больше чем заранее заложенная возможности модульности. Но это при условии что будет несколько проектов. Когда был 1, и решили сделать второй — такого предположения не должно быть.
Ну и пока делаем «чтобы просто работало» — вполне можно делать быстро и как получится, а когда стабилизированы хотелки, апи и прочее — тогда начать делать новую версию, с нуля, с нужными разделениями и вероятно на более подходящем языке. Не затягивать этот процесс, а то легаси с детским кодом потом много лет будет аукаться.
UFO just landed and posted this here
Если мы начинаем вносить правки в функционал блока C в зависимости от «хотелок» зависимых систем — уже что-то пошло не так! Тем более, если эти правки специфичны.

Дык вот реальность во многих коммандах именно такая. Плохо или с недостатком опыта обдуманное (см. ссылку на «Что видишь, то и есть») выделение "общего кода"…
Причем на моей памяти именно аргумент DRY, который не так легко паррировать в тех же самых кругах ;)

Однобоко. Правильно было бы сравнить проблемы со случаем не-выделения С. Те же доп. переключатели логики или версионность, при условии, что А и В плотно друг с другом взаимодействуют
Мы пишем макарона-стайл код, поэтому повторное использование не работает, понятненько?

Имхо, это один из вариантов "преждевременная оптимизация — корень всех зол".
Далеко не всегда однозначно можно сказать, что C нужно выделять. Кроме того, нужно понимать, что выделение C — это не столько про скорость разработки (это надо еще подумать, что быстрее — копипаста или унести кусок функционала в отдельный класс/модуль), а про стоимость поддержки.


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


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

Повторно используемая библиотека, пакет модуль, плагин или фреймвок — это продукты.
Если вы пишите отдельное приложение, но не библиотеку, пакет модуль, плагин или фреймвок, то повторное использование кода возможно только на уровне самого приложения.
3 системы и взаимодействие между ними не проще, чем 2 с частью похожего функционала. Если при проектировании B предполагается, что C будет использоваться не только в А и B, то есть смысл реализовать C как выделенный из B. Выделение С из уже работающего A это отдельная задача, требующая дополнительной оценки целесообразности.
Наконец, хотелось бы узнать ваше мнение или опыт крупномасштабного повторного использования. Когда оно работает, а когда терпит неудачу?


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

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


Стройте все изначально из правильных частей и не будет проблем с выделением.

Рецептом «изначально правильных систем» в условиях неопределённости и недостаточности сведений о будущих(через полгода-год) решениях бизнеса поделитесь?

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

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

Скорее в статье мне не хватает еще одной вещи…


С как компромис А и В (не говоря о Е и Д) уже сложнее просто суммы частного случая для А и частного (очень похожего) случая для Б.
Не всем очевидна эта истина.

Это не повторное использование. Это — совместное использование.
Повторное — это когда С берется из системы А, интегрируется в систему В и дальше системы А и В идут параллельными непересекающиеся курсами. И такой вариант даёт выигрыш для системы В по сравнению с "а давайте напишем С заново". Обычно под С заводится свой репозиторий, где для каждой системы(А, В и т.д.) заводится своя ветка С которая используется только в одной системе и нигде больше. А в транк сливают результаты фиксов в ветках и он используется только для ветвления при появлении нового проекта которому нужно С.
Тут нужно вспомнить о SOLID принципах, IoC и авто тестирование. Набраться опыта в данных сферах и сможете переиспользовать код.
И как переделать пилу в молоток?
А с точки зрения менеджмента и там и там одна металлическая часть и одна деревянная и оба инструмента нужны для работы по дереву.

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


  1. Автономность — модули можно создавать, тестировать и эксплуатировать независимо от любого конкретного набора других модулей. Это сразу требует, чтобы все зависимости и вся реализованная функциональность модулей описывались явными контрактами на уровне кода.
  2. Компонуемость — сборка приложения из набора модулей должна быть простой задачей, в идеале — решаемой автоматически.
  3. Рекурсивность — из набора модулей должен легко собираться композитный модуль, неотличимый от обычных модулей снаружи.
  4. Прозрачность — должна иметься возможность сгенерировать актуальную диаграмму компонентов для модулей в работающем приложении.
  5. Ортогональность — инструмент для организации модульности должен быть библиотекой, а не фреймворком. Правки в коде для поддержки модульности должны быть минимально возможными.
А можете написать, какие технологии использовали?
Для самого кода (к примеру, ASP.NET MVC + React), для композиции модулей (DI?)?
Кстати, а модуль — это что? Библиотека?
А можете написать, какие технологии использовали?

Технологии тут как раз нерелевантны из-за пункта 5.


  1. Изначально делалось на Delhi 2007
  2. Контрактами были COM-интерфейсы (только из-за наличия у них GUID)
  3. Модуль — объект, реализующий интерфейс IModule: GUID модуля, имя модуля, список реализованных интерфейсов, список интерфейсов-зависимостей, методы для инициализации и финализации.
  4. Из набора модулей автоматически строился граф зависимостей.
  5. Из графа автоматически генерировалась диаграмма компонентов на языке PlantUML.
  6. Порядок инициализации и наличие циклов определялось топологической сортировкой.
  7. Композитный модуль строился на основе графа зависимостей подмодулей. Его зависимости — все интерфейсы, которые подмодули не реализовали сами, а реализованные контракты — подмножество таковых у внутренних модулей.

Сейчас в свободное от основной работы время занимаюсь библиотекой для дотнета: https://github.com/Kirill-Maurin/FluentHelium


Доклад по теме (прошлый февраль): https://www.youtube.com/watch?v=Gd9Ze7-CIb0&t=1005s


DI-контейнеры как есть для модульности пригодны только ограниченно: с прозрачностью и рекурсивностью у них никак.


Надо бы найти время и сделать-таки серию постов.

Хм. Кажется что ваш интерфейс IModule нарушает ваше же требование ортогональности… Но в случае COM по-другому просто не сделать, ведь для coclass нельзя объявить особый конструктор.
Кажется что ваш интерфейс IModule нарушает ваше же требование ортогональности

Интерфейс — не нарушает.
Ортогональность соблюдена — и внутри, и снаружи модуля может быть что угодно, в отличие от требований фреймворков.
От COM взяли только базовые интерфейсы с GUID и счетчиками ссылок: Delphi в них умеет из коробки.
Реестр и прочее ActiveX барахло не использовалось.

Я говорю не о реестре. Интерфейс IModule является частью инструмента для организации модульности, и все модули которые его реализуют — он зависят от этого инструмента. Через этот интерфейс инструмент для организации модульности диктует модулям как они должны выглядеть. То есть инструмент для организации модульности становится (микро)фреймворком, а не библиотекой.
Фреймворк определяет структуру проекта.
Интерфейс же не мешает как реализовать модуль средствами на свой выбор, так и встроить реализованное в уже имеющийся проект.
Пример: двусторонняя интеграция с Autofac, можно как сделать модуль из контейнера, так и зарегистрировать модуль в вышестоящем контейнере
github.com/Kirill-Maurin/FluentHelium/blob/master/FluentHelium.Autofac/AutofacModuleExtensions.cs
Вот только чтобы сделать модуль из контейнера — вам нужно сначала вручную сформировать для него дескриптор, для чего нужно найти все зависимости. То есть сделать ту работу, которую обычно неявно делает Autofac…
То есть сделать ту работу, которую обычно неявно делает Autofac…

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

Он их определяет, только не собирает в один список.
Он их определяет, только не собирает в один список.

Этого он тоже не делает.
Максимум, что умеет Autofac: при попытке разрешения найти первую неразрешаемую зависимость или цикл, после чего выкинуть исключение или вернуть false для TryResolve().
Эта работа годится только для минимальной индикации ошибок конфигурации контейнера.
Для задач модульности это и слишком поздно, и слишком мало.
Так определяется непрозрачность и нерекурсивность контейнеров:


  1. Граф зависимостей неявный
  2. Автоматическое определение зависимостей работает для типов, но не для контейнеров.
  3. До попытки разрешения не работает практически ничего.

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

Вы осознаете что невозможно разрешить зависимость если само существование зависимости неизвестно? Очевидно, что внутри Autofac есть код который эти зависимости определяет, только результат его работы вам не виден.
  1. Нет никакой необходимости разрешать зависимости до тех пор, пока не начата инициализация модулей.
  2. Список неразрешенных зависимостей для графа модулей необходимо построить заранее: это позволяет разрешить их за счет ядра или заявить как зависимости композитного модуля.

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


Очевидно, что внутри Autofac есть код который эти зависимости определяет

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


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

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

Autofac тоже внутри ищет зависимости и разрешает их в правильном порядке. Но вторая задача намного проще первой.

И если я сам найду и определю все зависимости — мне уже не нужен Autofac, вот в чем проблема.
И если я сам найду и определю все зависимости — мне уже не нужен Autofac, вот в чем проблема.

Нет такой проблемы.


  1. То, что надо реализовать определяется не контейнером, а постановкой задачи.
  2. То, что надо выставить как зависимости — это НЕ список всех зависимостей в контейнере, это список только тех из них, которые не разрешаются внутри самого контейнера.
  3. Список таких зависимостей также определяется при постановке задачи — проектируя модуль, вы решаете, что он НЕ должен обеспечивать сам.
  4. Если уровнем выше использовать композитный модуль, то он сможет определить неразрешенные зависимости подмодулей автоматически и выставить их как свои — тут контейнер действительно может быть лишним.
  5. За связи, невидимые снаружи модуля, контейнер отвечает по-прежнему и нужен внутри модуля именно с
    этой целью.
  6. Простой для реализации модуль, конечно же, проще сделать без контейнера.
  7. В случае использования модуля, снаружи у вас может быть один контейнер, внутри другой, но оба они друг о друге ничего не знают и никак друг от друга не зависят.
  8. Это показатель реальной автономности модулей
Sign up to leave a comment.

Articles

Change theme settings