воскресенье, 15 марта 2015 г.

О роли сборок

В наших с вами интернетах (точнее, у меня в Г+) развернулась небольшая дискуссия на предмет того, когда стоит выделять сборки (.NET Assemblies) в приложении, а когда нет. Там же развернулась и еще одна дискуссия на предмет того, что считать слоем (layer), а что считать слоем (tier). Обсуждение последнего вопроса придется пока отложить, а сегодня я хочу поговорить подумать о том, когда нужно выделять сборки.

ПРИМЕЧАНИЕ
Вот Г+ запись (Лейеры, и Тиры, и Партиции), в которой я цитирую Лармана с объяснением исходной терминологии понятий layer, tier и partition.

Любой букварь про C# и .NET, коих написано over 9000, говорит о том, что сборка (assembly) – это единица развертывания (deployment). Там же обычно говорится о том, что они (сборки) версионны и что CLR обладает мощной системой поиска правильной версии, со всеми GAC-ами, assembly redirect-ами и прочей магией. Что dll hell позади, и теперь мы наконец-то можем заменить одну сборку приложения прямо в продакшне, не сломав все к чертям собачим!

Звучит это все просто отлично, но, если посмотреть по сторонам, то легко заменить, что разбиение системы на солюшены, проекты и пакеты (nuget packages) происходит по более сложному алгоритму. Вот некоторые критерии.

Независимое развертывание (deployment)

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

Применимость: для кода приложения – низкая; для повторноиспользуемых компонентов – средняя.

Вопросы безопасности (security)

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

Применимость: низкая.

Повторное использование кода (code reuse)

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

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

Ускорение процесса разработки

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

Применимость: средняя.

Модульность

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

Применимость: выше средней.

Cohesion/Coupling и другие страшные слова

Фундаментальные понятия проектирования, такие как low coupling (слабая связность), high cohesion (высокая связанность) и protected variation (защита от будущих изменений) применимы как на уровне классов, так и на уровне модулей или подсистем.

Физическая группировка классов в сборку позволят четко контролировать исходящие связи сборки, и ограничить число входных связей. Если один модуль (в ОО терминологии) очень цельный (highly cohesive), но при этом развивается разными темпами, то это может быть достаточным поводом для разбивания его на две сборки – стабильную и не стабильную (теперь часть клиентов завяжутся на стабильную часть и не будут подвергаться постоянным изменениям). Если высокая вероятность смены одной реализации на другую, то вполне возможно выделение интерфейсной сборки и динамической загрузки сборки с реализацией.

Применимость: выше средней.

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

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

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

Лично для меня, наиболее важными являются три последних критерия. Аспект независимого развертывания сводится практически к нулю. Наличие Continuous Delivery делает свое дело. Сейчас мало кто разворачивает отдельные куски приложения. Достаточно нажать одну кнопку, пересобрать весь MSI и выложить новую версию. Или нажать кнопку и залить новую веб-роль в Azure целиком. Нет особого смысла выкладывать лишь одну сборку. Все равно нужно прогнать все тесты, а сделать это путем подмены одной сборки обычно сложнее, чем за счет полного прогона на билд-сервере. Приложение может перейти на новую версию пакета (особенно, если пакет предоставлен третьей стороной), но и в этом случае проще пересобрать и выложить все приложение целиком. Выделять же пакетами куски своего приложения обычно довольно трудоемко, если речь не идет о широкоиспользуемым базовых компонентах.

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

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

7 комментариев:

  1. Сергей, спасибо за пост! Хочу поделиться своими мыслями на этот счёт:

    1. .NET-команда из JetBrains пришла к тому, что сборки - не совсем адекватный способ разбиения большой (это важно) системы на модули. Когда слишком много независимых осей развития продукта, то возможен комбинаторный взрыв количества сборок. Для R# осями являются поддерживаемые языки, разные окружения (Visual Studio разных версий, dotPeek, dotCover), возможности (навигация, автодополнение, рефакторинги) и т.д. Советую ознакомится с выступлением Сергея Шкредова на самом первом .NEXT, где он рассказывает про решение этой проблемы в рамках продуктов JetBrains: http://www.youtube.com/watch?v=1PsrPCgDQVY

    2. Бертран Мейер в своей книге "Object-Oriented Software Construction" писал, что "The class serves as both a module and a type". Сначала я с этой мыслью не мог согласиться, но после прослушивания выступления Сергея понял, что Мейер, наверное, прав. Что Вы думаете по этому поводу?

    ОтветитьУдалить
    Ответы
    1. Владимир, я посмотрю видео и отпишусь!

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

      Удалить
    2. Так что там с Мейером-то? :)

      Удалить
    3. Владимир, спасибо за видео. Основная идея в нем, насколько я понял, чтобы организовать зависимость не между сборками (dll), а в разрезе фич. Такие зависимости прописываются более детально на уровне классов входящих в фичу (с помощью аттрибутов). Таким образом, действительно, класс выступает и как тип и как модуль(развретывания).

      Удалить
  2. Этот комментарий был удален автором.

    ОтветитьУдалить
  3. Есть один аспект, который сильно мешает в разбиении проекта на сборки "по науке". К сожалению, производительность студии, решарпера и MSBuild сильно падает с ростом числа сборок и это вынуждает отказываться от разделения сборок даже там, где польза очевидна.

    ОтветитьУдалить
  4. Этот комментарий был удален автором.

    ОтветитьУдалить