суббота, 11 мая 2013 г.

Ветвление проекта, конфигурации и AssemblyName

Постановка задачи

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

Классический вариант. Два Application

Предположим, что наше решение имеет более-менее классический вид. Т. е. состоит из N-го количества библиотек (Class Library) и одного собирающего проекта (Application). Наиболее естественным решением нашей задачи будет создание двух собирающих (Application) проектов, в которых и будет находиться вся логика ветвления. При этом все общие части будут располагаться в библиотеках. Однако, такой подход потребует существенного рефакторинга всего решения - придется четко разделить код на общий и зависимый от версии. При этом, в Application должен будет остаться только второй вариант. Не могу сказать, что данные сложности непреодолимы, но их наличие мотивирует рассматривать и другие подходы тоже.

Ветвление на уровне конфигураций (Configuration)

Предположим, что у нас обычная команда кодеров, а не супер-слаженная группа гениальных программистов. Т. е. код структурирован плохо, библиотеки ссылаются друг на друга, в главном окне программы откуда-то взялась библиотечная логика. А про внедрение зависимостей (DI) половина нашей группы вообще не слышала. К сожалению, такая ситуация встречается сплошь и рядом и отрефакторить такое решение весьма не просто.
Рассмотрим другой подход - конфигурации. Для начала откроем Configuration Manager и добавим конфигурацию
Затем, идем в свойства проекта, в секцию "Build" и добавляем константу компилятора для нашей конфигурации
Далее расставляем директивы процессора везде, где это требуется. Например, так:
#if DEMO
    Console.WriteLine( "Это демонстрационная версия" );
#endif
Или так
#if DEMO
[assembly: AssemblyTitle( "ConsoleTest.Demo" )]
[assembly: AssemblyDescription( "Это демонстрационная версия проекта" )]
#else
[assembly: AssemblyTitle( "ConsoleTest" )]
[assembly: AssemblyDescription( "Это основная версия проекта" )]
#endif
[assembly: AssemblyConfiguration( "" )]
Ну и последний штрих - это название конечного продукта. Естественно, название конечного exe-файла для демо-версии должно отличаться от штатного названия. Иначе, мы будем просто путаться. Для этого нам нужно, что бы AssemblyName в разных конфигурациях был разным. Однако, Visual Studio не предоставляет нам такой возможности. Тем не менее, компилятор считывает название из файла проекта (.csproj), который мы можем править вручную в любом текстовом редакторе. Каждая конфигурация будет соответствовать секции
<PropertyGroup ...>
    ...
</PropertyGroup>
Поэтому, самое простое решение будет найти секцию, соответствующую нашей демо-версии и вписать внутрь элемент AssemblyName
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Demo|AnyCPU'">
    ...
    <AssemblyName>ConsoleTest.Demo</AssemblyName>
    ...
</PropertyGroup>
После этого достаточно запустить VS, открыть наше решение и скомпилировать все пакетным построением, отметив галочками основную и демо конфигурации. Однако, читая форумы, я обнаружил, что некоторые версии VS данный подход воспринимают плохо. Если у вас тоже возникли сложности, то можно использовать более длинный вариант:
1. В секции PropertyGroup объявляем какую-нибудь переменную
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Demo|AnyCPU'">
    ...
    <isDemo>true</isDemo>
    ...
</PropertyGroup>
2. После всех PropertyGroup добавляем секцию Choose, в которой и задаем наш AssemblyName
<Choose>
    <When Condition=" '$(isDemo)' == 'true' ">
      <PropertyGroup>
        <AssemblyName>$(AssemblyName).Demo</AssemblyName>
      </PropertyGroup>
    </When>
</Choose>
В некоторых случаях, такой подход работает стабильнее.
Ну и, конечно же, надо помнить, что сама студия по-прежнему не поддерживает наши добавки. Поэтому, запустить демо-версию из-под нее не удастся. Зато, компилятор отработает правильно и пакетная компиляция даст нам сразу обе версии - основную и демо.