Конкретные недостатки многих-малых собраний?

Я планирую некоторую работу по внедрению Dependency Injection в то, что в настоящее время является большой монолитной библиотекой, в попытке сделать библиотеку проще для модульного тестирования, легче для понимания и, возможно, более гибкой в ​​качестве бонуса.

Я решил использовать NInject, и мне очень нравится девиз Нейта "делай одно, делай это хорошо" (перефразируя), и, похоже, он особенно хорош в контексте DI.

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

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

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

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

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

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

РЕДАКТИРОВАТЬ: перенес анализ производительности и результаты в отдельный ответ (см. Ниже).

7 ответов

Решение

Я приведу вам пример из реальной жизни, где использование множества (очень) небольших сборок привело к.Net DLL Hell.

На работе у нас есть большой доморощенный каркас, длинный в зубе (.Net 1.1). Наряду с обычным канализационным кодом типа фреймворка (включая ведение журнала, рабочий процесс, создание очередей и т. Д.) Были также различные инкапсулированные объекты доступа к базе данных, типизированные наборы данных и некоторый другой код бизнес-логики. Я не был вокруг для начальной разработки и последующего обслуживания этой платформы, но унаследовал ее использование. Как я уже говорил, весь этот фреймворк привел к появлению множества небольших DLL. И, когда я говорю "многочисленные", мы говорим выше 100, а не о 8, которые вы упомянули. Еще более сложным было то, что ассамблеи были подписаны версионными версиями и должны были появиться в GAC.

Итак, перенесемся на несколько лет вперед и на несколько циклов обслуживания, и произошло то, что взаимозависимости от библиотек DLL и приложений, которые они поддерживают, привели к хаосу. На каждом рабочем компьютере имеется огромный раздел перенаправления сборки в файле machine.config, который обеспечивает загрузку "правильной" сборки Fusion независимо от того, какая сборка запрашивается. Это стало результатом трудностей, возникающих при перестройке каждой зависимой платформы и сборки приложения, которая зависела от той, которая была изменена или обновлена. Большие усилия (обычно) были предприняты, чтобы гарантировать, что не было внесено принципиальных изменений в сборки, когда они были модифицированы. Сборки были перестроены, и в machine.config была сделана новая или обновленная запись.

Вот где я остановлюсь, чтобы услышать звук огромного коллективного стона и удушья!

Этот конкретный сценарий является плакатом для ребенка, чтобы не делать. Действительно, в этой ситуации вы попадаете в совершенно неуправляемую ситуацию. Я вспоминаю, что мне потребовалось 2 дня, чтобы настроить машину для разработки на этой платформе, когда я впервые начал работать с ней - устранение различий между моим GAC и GAC среды выполнения, перенаправления сборок machine.config, конфликты версий во время компиляции из-за неверные ссылки или, что более вероятно, конфликт версий из-за прямой ссылки на компонент A и компонент B, но компонент B ссылался на компонент A, но отличную версию от прямой ссылки моего приложения. Вы поняли идею.

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

Вы могли бы подумать, что меня будут презирать, но я полагаю, что существует логическое физическое разделение для сборок - и когда я говорю здесь "сборки", я предполагаю одну сборку на DLL. Все это сводится к взаимозависимости. Если у меня есть сборка A, которая зависит от сборки B, я всегда спрашиваю себя, возникнет ли у меня необходимость ссылаться на сборку B без сборки A. Или есть ли преимущество для такого разделения. Глядя на ссылки на сборки, как правило, также хороший показатель. Если бы вы делили свою большую библиотеку на сборки A, B, C, D и E. Если вы ссылались на сборку A в 90% случаев, и из-за этого вам всегда приходилось ссылаться на сборки B и C, потому что A зависел от них тогда, вероятно, будет лучшей идеей объединять сборки A, B и C, если только нет действительно убедительных аргументов, позволяющих им оставаться разделенными. Корпоративная библиотека является классическим примером этого, когда вам почти всегда приходится ссылаться на 3 сборки, чтобы использовать один аспект библиотеки - однако в случае с корпоративной библиотекой существует возможность строить поверх основных функций и кода. повторное использование является причиной его архитектуры.

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

В любом случае, удачи!

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

Анализ:
Упомянутые выше конкретные недостатки, по-видимому, все сосредоточены на производительности одного вида другого, но фактические количественные данные отсутствовали, я провел некоторые измерения следующего:

  • Время загружать решение в IDE
  • Время компилировать в IDE
  • Время загрузки сборки (время, необходимое для загрузки приложения)
  • Оптимизация потерянного кода (время запуска алгоритма)

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

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

Приложение содержит 1000 классов, сгруппированных в 200 наборов по 5 классов, которые наследуются друг от друга. Классы называются Axxx, Bxxx, Cxxx, Dxxx и Exxx. Классы A полностью абстрактны, BD частично абстрактны, переопределяют один из методов A каждый, а E является конкретным. Методы реализованы так, что вызов одного метода в экземплярах E будет выполнять несколько вызовов по иерархической цепочке. Все тела метода достаточно просты, чтобы теоретически все они были встроены.

Эти классы были распределены по сборкам в 8 различных конфигурациях по 2 измерениям:

  • Количество сборок: 10, 20, 50, 100
  • Направление резания: по иерархии наследования (ни один из AE никогда не находится в одной сборке вместе), а также по иерархии наследования

Измерения не все точно измерены; некоторые были сделаны секундомером и имеют большую погрешность. Измерения сделаны:

  • Открытие решения в VS2008 (секундомер)
  • Составление решения (секундомер)
  • В IDE: время между началом и первой выполненной строкой кода (секундомер)
  • В IDE: время создания одного экземпляра Exxx для каждой из 200 групп в IDE (в коде)
  • В IDE: время для выполнения 100 000 вызовов на каждом Exxx в IDE (в коде)
  • Последние три измерения "в IDE", но из подсказки с использованием сборки "Release"

Результаты:
Открытие решения в VS2008

                               ----- in the IDE ------   ----- from prompt -----
Cut    Asm#   Open   Compile   Start   new()   Execute   Start   new()   Execute
Across   10    ~1s     ~2-3s       -   0.150    17.022       -   0.139    13.909
         20    ~1s       ~6s       -   0.152    17.753       -   0.132    13.997
         50    ~3s       15s   ~0.3s   0.153    17.119    0.2s   0.131    14.481
        100    ~6s       37s   ~0.5s   0.150    18.041    0.3s   0.132    14.478

Along    10    ~1s     ~2-3s       -   0.155    17.967       -   0.067    13.297
         20    ~1s       ~4s       -   0.145    17.318       -   0.065    13.268
         50    ~3s       12s   ~0.2s   0.146    17.888    0.2s   0.067    13.391
        100    ~6s       29s   ~0.5s   0.149    17.990    0.3s   0.067    13.415

Замечания:

  • Количество сборок (но не направление резания), по-видимому, оказывает приблизительно линейное влияние на время, необходимое для открытия раствора. Это не удивляет меня.
  • Примерно через 6 секунд время, необходимое для открытия решения, не кажется мне аргументом для ограничения количества сборок. (Я не измерял, оказало ли связывающее управление источником существенное влияние на это время).
  • Время компиляции увеличивается немного больше, чем линейно в этом измерении. Я полагаю, что большая часть этого связана с накладными расходами компиляции для каждой сборки, а не с разрешением символов между сборками. Я ожидаю, что менее тривиальные сборки будут лучше масштабироваться вдоль этой оси. Несмотря на это, я лично не считаю, что 30-е годы компиляции являются аргументом против разделения, особенно если учесть, что в большинстве случаев только некоторые сборки будут нуждаться в повторной компиляции.
  • Похоже, что едва заметное, но заметное увеличение времени запуска. Первое, что делает приложение, это выводит строку на консоль, время 'Start' - это время, которое потребовалось для появления этой строки с начала выполнения (обратите внимание, что это приблизительные оценки, потому что было слишком быстро измерить точно даже в худшем случае),
  • Интересно, что кажется, что загрузка сборки вне среды IDE (очень незначительно) более эффективна, чем внутри среды IDE. Вероятно, это как-то связано с подключением отладчика или чего-то подобного.
  • Также обратите внимание, что перезапуск приложения вне среды IDE еще больше сократил время запуска в худшем случае. Могут быть сценарии, когда 0,3 с для запуска недопустимы, но я не могу себе представить, что это будет иметь значение во многих местах.
  • Время инициализации и выполнения внутри IDE не зависит от разделения сборки; это может быть связано с тем фактом, что он нуждается в отладке, что облегчает распознавание символов в разных сборках.
  • За пределами IDE эта стабильность сохраняется, с одним предупреждением... количество сборок не имеет значения для выполнения, но при разрезании по иерархии наследования время выполнения на порядок хуже, чем при разрезании вдоль. Обратите внимание, что разница кажется мне слишком маленькой, чтобы быть системной; вероятно, это дополнительное время, которое требуется один раз для выполнения, чтобы выяснить, как выполнить те же самые оптимизации... честно говоря, хотя я мог бы исследовать это дальше, различия настолько малы, что я не склонен слишком беспокоиться.

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

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

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

Подход, который я пытаюсь использовать, заключается в следующем: пространства имен для логической организации. Сборки предназначены для группировки классов / пространств имен, которые должны физически использоваться вместе. То есть. если вы не ожидаете, что хотите ClassA, а не ClassB (или наоборот), они принадлежат одной сборке.

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

Недостатком многих проектов является то, что для компиляции (по крайней мере в VS) требуется некоторое время по сравнению с несколькими проектами.

Самым важным фактором в вашей организации сборки должен быть ваш график зависимостей как на уровне класса, так и на уровне сборки.

Сборки не должны иметь круговых ссылок. Это должно быть довольно очевидно, чтобы начать.

Классы, которые имеют наибольшее количество зависимостей друг от друга, должны быть в одной сборке.

Если класс A зависит от класса B, и, хотя B может не зависеть напрямую от A, он вряд ли когда-либо будет использоваться отдельно от A, тогда они должны совместно использовать сборку.

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

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

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

Я думаю, если вы говорите только о дюжине, у вас должно быть все в порядке. Я работаю над приложением со 100+ сборками, и это очень больно.

Если у вас нет способа управлять зависимостями - зная, что может сломаться, если вы измените сборку X, у вас проблемы.

Одна "приятная" проблема, с которой я столкнулся, это когда сборка A ссылается на сборки B и C, а B ссылается на V1 сборки D, а C ссылается на V2 сборки D. ("Искривленный алмаз" было бы неплохим названием для этого)

Если вы хотите иметь автоматическую сборку, вам будет весело поддерживать сценарий сборки (который необходимо будет построить в обратном порядке зависимостей), или же у вас будет "одно решение для управления ими всеми", что будет почти невозможно использовать в Visual Studio, если у вас много сборок.

РЕДАКТИРОВАТЬ Я думаю, что ответ на ваш вопрос очень сильно зависит от семантики ваших сборок. Могут ли разные приложения использовать одну сборку? Хотите иметь возможность обновлять сборки для обоих приложений отдельно? Вы собираетесь использовать GAC? Или скопировать сборки рядом с исполняемыми файлами?

Лично мне нравится монолитный подход.

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

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

Другие вопросы по тегам