Подтвердить что ты не робот

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

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

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

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

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

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

Ответ 2

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

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

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

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

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

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

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

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

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

  • Открытие решения в 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 секунд времени компиляции аргументом против расщепления, особенно, отмечая, что в большинстве случаев только некоторые сборки нуждаются в повторной компиляции.
  • Похоже, что это едва заметное, но заметное увеличение времени запуска. Первое, что делает приложение, - это вывод строки на консоль, время "Пуск" - это время, в течение которого эта строка появлялась с начала исполнения (обратите внимание, что это оценки, потому что она была слишком быстрой, чтобы точно измерять даже в худшем случае).
  • Интересно, что внешняя загрузка сборки IDE (очень немного) более эффективна, чем внутри IDE. Вероятно, это связано с усилием присоединения отладчика или некоторых таких.
  • Также обратите внимание, что повторный запуск приложения вне IDE уменьшил время запуска еще немного в худшем случае. Могут быть сценарии, где 0.3s для запуска неприемлемы, но я не могу себе представить, что это будет иметь значение во многих местах.
  • Инициализация и время выполнения внутри IDE являются твердыми независимо от разделения сборки; это может быть связано с тем, что ему нужно отлаживать, заставляя его иметь более легкое время при разрешении символов через сборки.
  • Вне среды IDE эта стабильность продолжается с одной оговоркой... количество сборок не имеет значения для выполнения, но при переходе по иерархии наследования время выполнения на дроби меньше, чем при разрезании. Обратите внимание, что разница кажется слишком малой для меня системной; вероятно, это лишнее время, когда требуется время выполнения, чтобы выяснить, как сделать те же самые оптимизации... откровенно, хотя я мог бы исследовать это дальше, различия настолько малы, что я не склонен слишком беспокоиться.

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

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

Ответ 3

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

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

Ответ 4

Монолитные монстры

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

С другой стороны, у многих проектов есть то, что он (по крайней мере, в VS) занимает довольно много времени, чтобы скомпилировать сравнение с несколькими проектами.

Ответ 5

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

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

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

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

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

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

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

Ответ 6

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

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

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

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

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

Ответ 7

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

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

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