Сергей Тарасов - Дефрагментация мозга. Софтостроение изнутри
В качестве решения перечисленных проблем появились так называемые двусторонние CASE-инструменты (two way tools), позволяющие редактировать как модель, непосредственно видя изменения в коде, так и, наоборот, менять код с полу– или полностью автоматической синхронизацией модели. Зачастую, такой инструмент был интегрирован прямо в среду разработки.
Рис. 17. Двусторонний CASE-инструментарий ModelMaker имеет возможности работы как с моделями, так и непосредственно с кодом приложения
Рис. 18. Работа с кодом приложения в ModelMaker
Нетрудно заметить, что двунаправленный подход в CASE-инструментарии в большей степени является мощным средством автоматизации отдельных программистов, так как обладает рядом ограничений:
• как правило, инструмент привязан к языкам и платформам;
• технология не выходит за рамки разработки конкретных программ и подсистем. То есть слои системы и архитектура остаются за рамками процесса;
• коллективная работа над моделями одновременно с кодом практически невозможна: приходится делить модели на независимые части, например подсистемы, разрабатываемые одним программистом;
• для достижения нужного эффекта методика по-прежнему требует навыков моделирования как минимум на уровне диаграммы классов. В противном случае CASE оказывается лишь очередным инструментов рефакторинга.
Следующим шагом в развитии автоматизированных средств софтостроения явилась программная фабрика – синтез подходов управляемой моделями разработки и архитектуры, генерирующий не только отдельные компоненты системы, но целые слои в соответствии с выбранной архитектурой и платформами.
На рынке уже имеется немало продуктов типа «software factory», если вы наберёте в поисковике эти ключевые слова, то получите множество ссылок на концепции и частные реализации. Например, неплохое руководство, хотя и привязанное к собственным средствам, составили в IBM[24]. Чтобы не утомлять вас текстами академического характера, в следующей главе я просто приведу пример одной фабрики под названием Genie Lamp (http://genielamp. sourceforge.net), применяемой непосредственно в различных моих проектах. Несмотря на то что подход УМР я использую с конца 1990-х годов, свести многие частные решения в несколько более общее удалось только за последние 2–3 года. Лень – двигатель прогресса, особенно когда надоедает переписывать генераторы кода и подстраивать относительно стандартные модели под частные требования.
Лампа, полная джиннов
Метафора системы достаточно проста: хочешь генерировать код компонента или слоя – попроси об этом соответствующего «джинна» в форме стандартного «заклинания». Джинны, как им и положено, живут в лампе.
Переходя к техническим терминам, программист описывает задачу в терминах логической модели, представляющей собой набор сущностей, их атрибутов, операций и связей между ними. Язык создан на основе XML, поэтому делать описания можно непосредственно руками в обычном текстовом редакторе.
Рис. 19. Общая схема работы с «лампой» и «джиннами»
Модель в виде XML-файлов поступает на вход «заклинателю» – входящей в состав пакета консольной утилите. Производятся проверки непротиворечивости модели, выдающие ошибки либо предупреждения разной степени важности. Во время анализа модель также преобразуется во внутренний формат в виде множества объектов с открытыми интерфейсами доступа.
Если модель корректна, «заклинатель» начинает призывать «джиннов» сделать свою работу, передавая каждому на вход кроме самой модели ещё и разнообразные параметры, конфигурацию, касающуюся не только самих джиннов, но и, например, таких настроек, как правила именования в конкретном слое системы.
Обработав модель в соответствии с конфигурацией проекта, джинн выдаёт готовый к компиляции в среде разработки код. Для слоя хранения данных кроме генерации специфичных для СУБД SQL-скриптов производится их прогон на заданном сервере разработки.
В случаях, когда система уже существует и подлежит, например, переделке, можно восстановить модель из схемы базы данных. Конечно, даже теоретически такое восстановление не может быть полным из-за разницы в семантике, но большую часть рутинной работы оно выполняет. Проведя один раз импорт, далее мы редактируем, структурируем модели и продолжаем работать только в обычном цикле изменений «через модель».
На что похожа логическая модель? Приведу пример описания из рабочего проекта, содержащего один пользовательский тип, один перечисляемый тип, две сущности и одну связь (отношение) между ними.
Пример модели в Genie Lamp
<Type name="TEntityId" baseType="int" />
<Enumeration name="Granularity">
<Doc><Label lang="ru">Грануляция учётного периода</Label></Doc>
<Item name="Day" value="0">
<Doc><Label lang="ru">День</Label></Doc>
</Item>
<Item name="Month" value="1" default="true">
<Doc><Label lang="ru">Месяц</Label></Doc>
</Item>
<Item name="Year" value="2">
<Doc><Label lang="ru">Год</Label></Doc>
</Item>
</Enumeration>
<Entity name="FiscalYear">
<Doc><Label lang="ru">Финансовый год</Label></Doc>
<Attribute name="Id" type="TEntityId" primaryid="true" autoincrement="true" />
<Attribute name="Name" type="TCaption" uniqueid="true">
<Doc><Label lang="ru">Обозначение года</Label></Doc>
</Attribute>
<Attribute name="Granularity" type="Granularity">
<Doc><Label lang="ru">Грануляция периодов</Label></Doc>
</Attribute>
<Attribute name="FromDate" type="date">
<Doc><Label lang="ru">Дата начала</Label></Doc>
</Attribute>
<Attribute name="ToDate" type="date">
<Doc><Label lang="ru">Дата окончания</Label></Doc>
</Attribute>
<Attribute name="Closed" type="boolean" default="false">
<Doc><Label lang="ru">Год закрыт?</Label></Doc>
</Attribute>
<Attribute name="GranularityName" type="string" persisted="false">
<Doc>
<Text lang="ru">Возвращает локализованое название грануляции</Text>
</Doc>
</Attribute>
<Operation name="CreatePeriods" access="public">
<Doc>
<Text lang="ru">
Создает периоды финансового года
между датами начала и окончания
в соответствии с грануляцией. Например, для фин. года,
совпадающего с календарным, и помесячной грануляцией
будут созданы 12 месячных периодов
</Text>
</Doc>
<Returns type="void"/>
</Operation>
<Operation name="FindPeriodIdByDate" access="public">
<Doc>
<Text lang="ru">
Возвращает ID периода по заданной дате, "0" если не найден
</Text>
</Doc>
<Param name="periodDate" type="datetime"/>
<Returns type="TEntityId"/>
</Operation>
<Operation name="DeleteCascade" access="public">
<Returns type="void"/>
</Operation>
</Entity>
<Entity name="Period">
<Doc><Label lang="ru">Учётный период</Label></Doc>
<Attribute name="Id" type="TEntityId" primaryid="true" autoincrement="true" />
<UniqueId>
<Attribute name="FiscalYearId" type="TEntityId">
<Doc><Label lang="ru">ID финансового года</Label></Doc>
</Attribute>
<Attribute name="FromDate" type="date">
<Doc><Label lang="ru">Дата начала</Label></Doc>
</Attribute>
</UniqueId>
<UniqueId>
<OnAttribute name="FiscalYearId"/>
<Attribute name="PeriodNumber" type="smallint">
<Doc><Label lang="ru">Номер периода</Label></Doc>
</Attribute>
</UniqueId>
<Attribute name="ToDate" type="date">
<Doc><Label lang="ru">Дата окончания</Label></Doc>
</Attribute>
</Entity>
<Relation entity="Period" name="FiscalYear"
entity2="FiscalYear" name2="Periods"
cardinality="M:1">
<AttributeMatch attribute="FiscalYearId" attribute2="Id" />
</Relation>
Теперь необходимо задать конфигурацию в описании проекта. Предположим, что мы хотим создать 3-звенное приложение со следующими логическими слоями:
• слои хранения будут развёрнуты на SQL Server или Oracle;
• слой домена под управлением NHibernate;
• слой веб-служб на базе ServiceStack (вместо WCF, имеющего под Mono/Linux ограничения).
Пример конфигурации проекта в Genie Lamp
<! – Включаем файл(ы) модели в проект – >
<ImportModel fileName="MyModel.xml" />
<! – Будем использовать джинна SQL Server – >
<Genie name="SqlServer"
type="GenieLamp.Genies.SqlServer.SqlServerGenie"
assembly="GenieLamp.Genies.SqlServer"
active="false"
outDir="%PROJECT_DIR%/../SQL/SqlServer-%TARGET_VERSION%"
outFileName="%PROJECT_NAME%.sql"
updateDatabase="true"
targetVersion="2008">
<Param name="Database.Create" value="false" />