Я работаю в компании, которая занимается разработкой заказных учётных систем уже достаточно долго. Примерно столько же времени существует технологический отдел — команда людей, которые разрабатывают инструменты и фреймворки для своих товарищей, а те уже в свою очередь создают системы для заказчиков. Кратко можно сказать, что мы (технологи) унифицируем процесс написания бизнес-логики за счёт собственного ORM, объединяем проектировщиков и программистов за счёт UML-редактора с генераторами кода, предоставляем различные UI элементы управления, позволяющие продуктивно работать пользователям конечных приложений.
В этой статье я хочу поделиться своим опытом относительно процесса создания технологического слоя.
Зачем нам фреймворки?
Есть несколько причин, почему компания содержит несколько человек, которые не приносят прямой прибыли, вот, на мой взгляд, самое главное:
1. Переиспользование идей и кода. Технологический отдел собирает лучшие практики по мере выполнения разных проектов и распространяет их на все команды. В данном случае нет необходимости устраивать связи каждого с каждым, чтобы быть в курсе что нового и полезного появилось у соседа. Ведь сосед может сразу делится с технологами и сильно не заморачиваться над универсализацией, а у технологов есть возможность проинформировать всех остальных (периодически формируется список доработок, которые попали в очередную версию технологических продуктов).
2. Унификация решений. Программисты иногда меняют работу. Чтобы бороться с негативными моментами таких ситуаций технология предоставляет некоторые унифицированные методы для решения типичных задач. Таким образом снижается количество велосипедов и сокращается время вхождения нового человека в проект.
Точки роста или взросление технологии
Немного о том, как у нас зарождается новое технологическое направление. Время от времени применяемые платформы видоизменяются и приходится создавать технологию под эти новинки.
Детский сад
Ввиду того, что наш продукт не является законченным приложением, для начала выбирается любая предметная область для проекта-прототипа (1-3 шт.). Это приложение служит только для проверки решений и подходов, которые мы планируем реализовать. Ради шутки последняя такая система у нас служила для учёта домашних кошек в квартире. Особенностью прототипов является то, что код довольно сильно перемешан и непонятно где технологический слой, а где уже начинается прикладная логика. На этом «кошачьем» проекте прорабатываются все необходимые компоненты, генераторы, элементы пользовательского интерфейса. Над проектом работают только технологи.
Школьная пора
Когда наступает пора опробовать технологию на чём-то посерьёзнее, выбирается «жертвенный» проект из коммерческих. Как правило, он имеет в 1,5 — 2 раза больший срок, который может быть отведён на разработку, чем типичный проект (все понимают что есть риски, т.к. технология применяется нами впервые и за технологические работы платит сама компания, а не заказчик). Особенностью такого проекта является то, что технологический код изначально находится в солюшене, и по мере разработки разделяется на технологические проекты и прикладной код (который зависит от предметной области). Над проектом работают и технологи и прикладные программисты. Следует понимать, что всё ещё идёт отладка основных подходов и не всё в технологии готово на 100%, поэтому проект будет с врождёнными «велосипедами», усложняющими поддержку. Иногда эти «велосипеды» заменяются на стандартные технологические решения, иногда так и остаются до конца.
Высшее образование
На данном этапе считаем что технология готова к широкому применению. Переходим в нормальный «рабочий режим». Прикладные разработчики больше не нуждаются в непосредственной помощи со стороны технологов. Технологические сборки поставляются в скомпилированном виде. Задачи на доработку и сообщения об ошибках передаются в TFS-проект технологии.
Профессиональное развитие
Время от времени требуются серьёзные доработки технологии, как правило, это добавление функциональности, например, общая подсистема отчётов, подсистема полномочий, подсистема логирования и т.п… Мы стараемся применять что-то наподобие Feature driven development: ставится цель реализовать требуемую фичу целиком, и для пользователей она появляется уже в законченном виде. Как правило, такие новинки сразу становятся востребованными всеми проектами, ведущими активную разработку. Забавно что иногда, под конец разработки очередной фичи, приходит прикладной программист и просит реализовать именно это.
Выход на пенсию
По мере развития новых направлений старые постепенно сходят на нет. Проекты закрываются, остаются единицы проектов, которые требуют поддержки. Вместе с ними и технология «выходит на пенсию», переходит в режим Bug driven development. Новых фич уже не добавляется, происходит только исправление старых багов, которые копятся годами. Часть багов исправляется костылями в прикладном проекте, поскольку вносить изменения в технологию уже нецелесообразно. Изменения в технологии всегда нужно вносить с оглядкой на все проекты, которые от неё зависят. Одно неверное движение и проблемы гарантированы как людям на прикладном проекте, так и вслед за ними и технологам.
Если что-то всё-таки нужно переписать, то для технологии со множеством вхождений в проекты:
• Собираем требования, которым удовлетворял старый функционал (как правило после череды доработок и небольших исправлений цельной и актуальной документации для программиста не остаётся)
• Планируем работы (ещё раз смотрим на бюджет и оцениваем целесообразность)
• Пишем тесты чтобы зафиксировать состояние. По ним будем определять что не сломается уже то, что работало раньше.
• Пишем функционал
• Проверяем качество и соответствие заявленным требованиям
Техническая сторона вопроса
Теперь немного о техниках и приёмах. Речь пойдёт о Microsoft .NET Framework, поскольку наши решения построены именно на этой платформе.
Точки расширения
Технология не должна ограничивать программистов, а давать преимущества по сравнению с решением с нуля, а иногда целиком готовые решения. Для того, чтобы программист мог проявить свою индивидуальность там, где технология «слишком стандартна» мы используем следующие подходы:
• Наследование. Есть базовый класс, который реализует стандартное поведение и располагается в технологической сборке. Генерируемый для прикладного проекта код наследуется от базового класса и в специальных методах предполагает изменение стандартного поведения.
• События. Технологические классы содержат ряд событий, обработка которых позволяет изменить поведение. Следует помнить, что на событие могут подписаться много раз.
• Делегаты. Аналогично событиям, но реализация может быть только одна. Это предохраняет от нежелательного срабатывания несколько раз.
• App.config. Различные мелочи попадают в конфигурационный файл. Как правило, это некоторые флаги и пути (нужно ли писать отладочную информацию, строки подключения к сервисам и т.п.).
• DI. Этот вариант подразумевает переопределение целого модуля.
Логирование событий
Из технологического слоя редко есть нормальный доступ к интерфейсу пользователя. Как правило, если что-то пошло не так, это говорит о наличии ошибки либо в технологии (да-да, они есть и мы это понимаем), либо в прикладном коде (в идеальном случае можно указать прикладному программисту явно что он делает не так и в каком месте). В любом случае, надо как-то сигнализировать об этом. Мы применяем Log4net. Для выделения технологических сообщений можно использовать либо отдельный логгер, либо закрепить уровень ошибок, например Warn, за технологией.
XML-комментарии
XML-комментарии в технологическом коде крайне важны. Нужно обязательно прописывать их для публичных классов, методов и свойств. Это позволит выполнить генерацию справки (http://sandcastle.codeplex.com/) и обеспечит подсказки в прикладных проектах благодаря IntelliSense. Не забывайте в настройках проекта указывать необходимость генерации xml-файла с комментариями.
Breaking changes
Крайне редко происходит такое изменение в технологии, что требует вмешательство прикладного программиста чтобы обновить технологический слой. Если такое обновление можно предсказать, то нужно предупреждать заранее, например, при помощи атрибута Obsolete:
[Obsolete(“Эта функция будет удалена в очередной версии, используйте функцию ”)]
Иногда приходится идти на поводу и применять правило: «лучше 1 раз добавить, чем 2 раза обновить», т.е. если сигнатура метода используется в 1000 мест в каждом прикладном проекте, менять её в технологии — не самая удачная идея. Самое главное правило: всегда детальное описание возможных проблем с обновлением — предупреждён, значит вооружен.
Документирование
Для технологических сборок крайне важно наличие документации. Для хранения информации мы используем Wiki-движок с правом записи у технологов. Для поддержания информации в актуальном состоянии применяется 2 правила:
1. Сделал дело – запиши в Wiki!
2. Ответил на вопрос – запиши в Wiki!
Уровни описания:
1. Для себя (как устроено)
2. Для программистов прикладных проектов (как использовать методы и классы)
3. Для пользователей систем (как использовать компоненты)
Структура проектов фреймворка
Иногда требуется минимизировать количество сборок, выдаваемых на проекты. Это позволяет бороться с неправильным обновлением, когда требуется обновить несколько сборок сразу, иначе что-нибудь не будет работать и минимизирует риск того, что что-то забудут установить заказчику. Однако разным проектам нужны несколько разные функции. Что можно применять?
1. Ссылки на cs-файлы между проектами (немного усложняется работа с проектами)
2. ILMerge (это можно поручить Build-серверу, но затрудняется отладка)
3. Сборки в ресурсах (с отдельным механизмом загрузки сборок)
Подписывание сборок или Strong name
Ещё одно железное правило: все технологические сборки должны быть подписаны. Это нам даёт следующие преимущества:
1. Подписанные сборки однозначно идентифицируются в GAC
2. Подписать можно только если все зависимые сборки подписаны (прикладные сборки не смогут подписать, если технологические сборки не будут подписаны)
3. Некоторая степень защиты (чтобы изменить код нужно будет пересобрать все сборки приложения)
Если так случается что код от сборки утерян, то подписывание можно сделать пересобрав сборку. Пересборка выполняется таким образом (для указания файла с ключом поищите специальный параметр):
1. ildasm.exe sampleA.exe /source /out:sampleA.il
2. ilasm.exe sampleA.il /exe /out:sampleA.exe
Стратегия выпуска версий
Чем чаще происходит выпуск продукта, тем меньше пользователям кажется что программисты ничего не делают. Мы опробовали 2 варианта выпуска версий технологических продуктов:
1. Последние сборки самые лучшие
Всё что размещено в SourceControl считается проверенным и прикладные программисты могут брать любой серверный билд технологических сборок. При этом технологи перед каждым Check-in-ом должны тщательно проверить свои изменения.
Плюсы:
• Моментальная публикация изменений
Минусы:
• Каждый Check-In требует расширенной проверки с большим количеством разной функциональности, которая может пострадать
• Ошибки могут легко проникнуть в версию у заказчика
2. Периодический выпуск стабильной версии
Выпуск версии технологических сборок на периодической основе.
Плюсы:
• Более надёжная интеграционная проверка
Минусы:
• Есть риск потерять контроль над кодом в середине итерации
• Требуется время чтобы всё проверить ещё раз, никаких моментальных исправлений
Во время выпуска версии на 1-2 дня все технологи отвлекаются на проверку и исправление найденных ошибок. Именно этот вариант у нас сейчас применяется.
Версии сборок
Есть 2 стратегии обозначения версий сборок:
1. Изменяемые версии сборок
• Обновление прикладных проектов только с перекомпиляцией проекта (невозможность отправить заплатку в технологии прямо заказчику)
• Часть проблем будет видна при компиляции (актуально при сложных зависимостях между сборками)
• Проблема с сериализацией типов (сериализованные данные, хранящиеся у заказчика могут вызвать проблемы при обновлении версий сборок)
2. Фиксированные версии сборок
• Обновление простой подменой сборок
• Проблемы будут видны при запуске т.к. перекомпиляция при обновлении не требуется
• Можно отправить только исправленную сборку заказчику в крайнем случае
• Дата выпуска сборки в качестве условной версии для идентификации
Наша команда остановилась на втором варианте т.к. часто приходится проверять новые технологические сборки на прикладных проектах, а перекомпилировать их нет ни желания ни времени. У нас в Team Foundation Server настроены серверные билды и только серверный билд может быть передан на внедрение.
Бренчинг: за и против
Мы рассматривали 2 подхода к организации хранения исходного кода:
Ветви для добавления фич, ветви для внедрений
Плюс:
• Можно внести небольшое изменение прямо в нужную версию
Минус:
• Нужно выполнять много слияний кода
Единая версия без веток
Плюс:
• Нет слияния
Минус:
• Версия, находящаяся в разобранном состоянии не поддаётся быстрой починке
Мы остановились на более простом втором варианте. Стараемся не делать длинных по времени изменений без Check-In-ов, но и Check-in-ы не должны рушить работоспособность наших решений.
Внешние сборки
Пара слов об OpenSource в коммерческом продукте: при желании можно найти OpenSource решения с хорошей лицензией для решения какой-нибудь нужной задачи. Однако, при принятии такого решения следует сильно подумать над тем, как туда будут вноситься изменения:
1. Править всё самим локально
Плюс:
• Можно делать всё что угодно
Минус:
• При обновлении официальной версии придётся самостоятельно выполнять слияние своих изменений
2. Посылать обновления команде, поддерживающей проект
Плюс:
• Дополнительная проверка на качество исправлений
Минусы:
• Риск, что поддержка прекратится
• М.б. длительный срок проверки и включения исправлений в релиз
• Исправления могут быть отвергнуты по той или иной причине
Иногда приходится делать выбор в пользу платного решения только потому что политика внесения изменений (как минимум исправления ошибок) фиксируется в лицензии, за которую платятся деньги.
Обфускация
Как защищать от дизассемблирования код, который размещён доступен как из прикладных, так и из технологических сборок?
Можно обфусцировать перед передачей на проект технологические сборки, но тогда мы получаем:
Плюс:
• Прикладные программисты не будут совать нос в технологический код и задавать глупые вопросы по этому поводу
Минусы:
• Сложность отладки для прикладных разработчиков
• Возможная нестабильность работы во время разработки
• Обфускация и проверка будет выполнена дважды
Второй вариант — обфускация целиком готового приложения со всеми технологическими сборками.
Плюсы:
• Обфускация на этапе сборки приложения
• Возможность отладки приложения с заходом в код фреймворка Верификация работы приложения выполняется единожды
Минус:
• Защитой технологических сборок занимаются прикладные программисты
Мы применяем последний вариант.
Тестирование
Не буду говорить про модульные тесты — это нужно любому слою, не только технологическому. Однако, всегда технологический код должен быть написан с учётом требований к производительности и безупречно работать в многопоточном режиме.
Тестирование технологических проектов, как правило, невозможно без конечного приложения, в котором описана предметная область. В некоторых случаях чтобы понять где именно скрывается ошибка приходится создавать своё отдельное приложение и пытаться повторить необходимые условия. Только такой вариант помогает отделить «наводки» из прикладного кода.
Заключение
Могу отметить, что работа над технологией — весьма увлекательное и местами забавное увлечение. Главное — не увлекаться слишком сильно и иногда общаться с конечными пользователями, стараясь понять их потребности. Спасибо всем кто дочитал до конца этот длинный монолог.
Автор: bratchikov