Два основных объявления на конференциях PDC09 и Mix10 были посвящены выпуску бета-версии и релиз-кандидату Silverlight 4 соответственно. К моменту, когда вы будете читать эту статью, окончательная версия Silverlight 4 будет доступна для скачивания. Наряду с обширной поддержкой печати, она включает поддержку повышенных разрешений, веб-камер, микрофонов, «тостов» (toasts), доступа к буферу обмена и др. Благодаря новому набору всяческих средств Silverlight 4 уже наступает на пятки Adobe AIR как многоплатформенная инфраструктура UI с богатой функциональностью.
Хотя все это очень интересно, я прежде всего являюсь разработчиком бизнес-приложений, и мне особенно нравится простой способ включения бизнес-данных и логики в приложение Silverlight.
Одна из проблем со специализированными бизнес-приложениями Silverlight — подключение к данным. В Silverlight 3 ничто не мешает вам создать свой WCF-сервис (Windows Communication Foundation) и соединяться с ним, но такой вариант оставляет желать много лучшего, особенно если вы вспомните о бесчисленных способах подключения к данным в настольных или ASP.NET-приложениях. Если настольные и веб-приложения могут подключаться к базе данных напрямую через NHibernate, Entity Framework (EF) или чистые конструкции ADO.NET, то приложения Silverlight отделены от моих данных «облаком». Я называю такое отделение пропастью (data chasm).
Преодолеть эту пропасть может показаться обманчиво простым делом. Очевидно, что в какой-то мере это достигнуто в ряде существующих приложений Silverlight, активно работающих с данными. Но то, что поначалу кажется простым, становится все сложнее и сложнее по мере того, как вы сталкиваетесь с очередными проблемами. Как отслеживать изменения через сеть или инкапсулировать бизнес-логику в сущности, которые находятся по обе стороны брандмауэра?
Для решения этих проблем сейчас появляются сторонние инструменты, но Microsoft тоже видит необходимость в том, чтобы предоставить свое решение, поэтому были созданы WCF RIA Services (ранее .NET RIA Services), или RIA Services для краткости. (Полное введение в RIA Services см. в статье «Building a Data-Driven Expense App with Silverlight 3» в майском номере MSDN Magazine за 2009 г. по ссылке msdn.microsoft.com/magazine/dd695920.) Я был в курсе всех наработок с того момента, как меня впервые пригласили к участию в программе тестирования бета-версии; я вносил предложения для группы разработки и учился применению инфраструктуры в своих приложениях.
Распространенный вопрос на форумах RIA Services — какое место занимают RIA Services в архитектуре передовых решений. На меня всегда производили впечатление базовые средства «форм поверх данных» в RIA Services, но я всегда четко видел возможность улучшить архитектуру своего приложения, чтобы проблемы инфраструктуры не проникали в его логику.
Представляю KharaPOS
Я создал приложение-пример KharaPOS, иллюстрирующее концепции, которые я намерен представить в этой статье. Это приложение кассового терминала (point-of-sale, POS), реализованное на Silverlight 4 с применением RIA Services, Entity Framework и SQL Server 2008. Конечная цель — создать возможность хостинга этого приложения на платформе Windows Azure и SQL Azure, но в поддержке Windows Azure со стороны Microsoft .NET Framework 4 есть маленькая проблема (или просто отсутствие нужной поддержки).
В то же время KharaPOS — хороший пример использования .NET Framework 4 для создания реального приложения. Проект размещен через CodePlex на KharaPOS.codeplex.com. Вы можете посетить этот сайт и скачать исходный код, просмотреть документацию и присоединиться к дискуссии по разработке этого приложения.
Должен отметить, что большую часть архитектуры и функциональности KharaPOS я позаимствовал из книги Питера Соуда (Peter Coad) в соавторстве с Дэвидом Нортом (David North) и Марком Мэйфилдом (Mark Mayfield) «Object Models: Strategies, Patterns, and Applications, Second Edition» (Prentice Hall PTR, 1996). Я сосредоточусь на одной подсистеме этого приложения — управлении каталогом (рис. 1).
Рис. 1. Entity Data Model для Catalog Management
Корпоративные шаблоны
Проектировочные шаблоны для разработки корпоративных приложений обсуждаются в целом ряде превосходных книг. Одну из таких книг я постоянно использую как справочник:Мартин Фаулер (Martin Fowler) «Patterns of Enterprise Application Architecture» (Addison-Wesley, 2003). Эта книга и дополняющий ее веб-сайт (martinfowler.com/eaaCatalog/) предоставляют полезные шаблоны для создания бизнес-приложений.
Большое количество шаблонов в каталоге Фаулера относятся к представлению данных и манипуляциям с ними; кроме того, что интересно само по себе, они занимают то же место, что и RIA Services. Понимание этого даст вам более четкую картину того, как адаптировать RIA Services под требования разнообразных бизнес-приложения — от простейших до самых сложных. Я рассмотрю следующие шаблоны:
- формы и элементы управления;
- сценарий транзакции;
- модель предметной области;
- уровень прикладного сервиса.
Давайте кратко обсудим эти шаблоны. Первые три относятся к различным способам включения логики, связанной с данными. По мере их освоения вы постепенно перейдете от разбрасывания этой логики по всему приложению и ее дублирования везде, где в ней возникает необходимость, к ее централизации.
Формы и элементы управления
Шаблон «формы и элементы управления» (forms-and-controls pattern) (или, как я называю его, «формы поверх данных») помещает всю логику в UI. На первый взгляд это кажется скверной идеей. Но для простого ввода данных и просмотра в виде «основные сведения/детали» (master-detail) это самый простой и самый прямой подход к связыванию UI с базой данных. Во многих инфраструктурах есть встроенная поддержка этого шаблона (например, в Ruby on Rails, ASP.NET Dynamic Data и SubSonic), так что у него явно есть свое место, хотя некоторые называют его «антишаблоном». Многие разработчики низводят подход «формы поверх данных» до уровня начального прототипа, но все же у него имеется своя область применения в конечных приложениях.
Независимо от вашего мнения насчет его полезности нельзя отрицать его простоту. Такой подход не называют быстрой разработкой приложений (rapid application development, RAD), так как он весьма утомителен. RAD в Silverlight вносят WCF RIA Services. Используя Entity Framework, RIA Services и Silverlight Designer, на основе «форм поверх данных» можно создать простой редактор таблицы базы данных буквально в пять шагов.
- Создайте новое бизнес-приложение Silverlight.
- Добавьте в него новую Entity Data Model (EDM) (используйте мастер для импорта базы данных).
- Добавьте сервис предметной области (domain service) в веб-приложение (не забудьте сначала скомпилировать его, чтобы обнаружение EDM прошло должным образом), которое ссылается на модель данных.
- Используйте секцию источников данных и перетащите сущность, предоставляемую через RIA Services, на поверхность страницы или пользовательского элемента управления в приложении Silverlight (и вновь скомпилируйте его, чтобы оно могло увидеть новый сервис предметной области).
- Добавьте кнопку и отделенный код для сохранения изменений на форме в базу данных; для этого достаточно такой строки:
this.categoryDomainDataSource.SubmitChanges();
Теперь у вас есть простая сетка с данными, с помощью которой можно напрямую редактировать существующие записи в вашей таблице. Добавив еще несколько элементов, вы сможете создать форму, которая позволит включать в таблицу новые записи.
Хотя этот шаблон неоднократно демонстрировался, чтобы показать преимущества использования RAD с WCF RIA Services, его все равно стоит привести здесь, так как он обеспечивает базовую разработку с применением этой инфраструктуры. Кроме того, как уже упоминалось, это вполне допустимый шаблон в приложениях на основе RIA Services.
Рекомендация Как и в случае динамических данных ASP.NET, шаблон «формы поверх данных» следует применять для несложных UI администрирования (как в редакторе категории продукта в KharaPOS), где логика проста и прямолинейна: добавление, удаление и изменение записей в таблице. Но, как мы сейчас увидим, Silverlight и RIA Services масштабируются до гораздо более сложных приложений.
Шлюз к табличным данным Стандартный подход к службам RIA Services, который я сейчас исследовал, можно также рассмотреть как реализацию шаблона шлюза табличных данных, как показано на стр. 144-151 книги Фаулера. Используя два уровня абстракции (EF над базой данных и сервис предметной области над EF), я создал простой шлюз к таблицам базы данных, в которых применяются операции CRUD (Create, Read, Update, Delete) и которые возвращают DTO-объекты (Data Transfer Objects) со строгой типизацией.
С технической точки зрения, это нельзя назвать настоящим шлюзом к табличным данным из-за двух уровней абстракции. Но если не придираться, он очень сильно напоминает шаблон шлюза к табличным данным. Если честно, было бы логичнее обсудить сопоставление между RIA Services и шаблоном шлюза к табличным данным, так как все остальные шаблоны в списке являются шаблонами интерфейсов данных, но «формы поверх данных» — это главным образом шаблон UI. Однако мне кажется более целесообразным начать с базового сценария и сфокусироваться на UI, а потом двигаться от него к базе данных.
Model-View-ViewModel (MVVM) Хотя создать функциональную форму на основе «форм поверх данных» несложно, кое-какие трения возникнуть могут. Иллюстрацией может послужить рис. 2, где показан XAML для управления категориями.
Рис. 2. XAML для управления категориями
<Controls:TabItem Header="Categories">
<Controls:TabItem.Resources>
<DataSource:DomainDataSource
x:Key="LookupSource"
AutoLoad="True"
LoadedData="DomainDataSourceLoaded"
QueryName="GetCategoriesQuery"
Width="0">
<DataSource:DomainDataSource.DomainContext>
<my:CatalogContext />
</DataSource:DomainDataSource.DomainContext>
</DataSource:DomainDataSource>
<DataSource:DomainDataSource
x:Name="CategoryDomainDataSource"
AutoLoad="True"
LoadedData="DomainDataSourceLoaded"
QueryName="GetCategoriesQuery"
Width="0">
<DataSource:DomainDataSource.DomainContext>
<my:CatalogContext />
</DataSource:DomainDataSource.DomainContext>
<DataSource:DomainDataSource.FilterDescriptors>
<DataSource:FilterDescriptor
PropertyPath="Id"
Operator="IsNotEqualTo" Value="3"/>
</DataSource:DomainDataSource.FilterDescriptors>
</DataSource:DomainDataSource>
</Controls:TabItem.Resources>
<Grid>
<DataControls:DataGrid
AutoGenerateColumns="False"
ItemsSource="{Binding Path=Data,
Source={StaticResource CategoryDomainDataSource}}"
x:Name="CategoryDataGrid">
<DataControls:DataGrid.Columns>
<DataControls:DataGridTextColumn
Binding="{Binding Name}" Header="Name" Width="100" />
<DataControls:DataGridTemplateColumn
Header="Parent Category" Width="125">
<DataControls:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox
IsSynchronizedWithCurrentItem="False"
ItemsSource="{Binding Source=
{StaticResource LookupSource}, Path=Data}"
SelectedValue="{Binding ParentId}"
SelectedValuePath="Id"
DisplayMemberPath="Name"/>
</DataTemplate>
</DataControls:DataGridTemplateColumn.CellEditingTemplate>
<DataControls:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Parent.Name}"/>
</DataTemplate>
</DataControls:DataGridTemplateColumn.CellTemplate>
</DataControls:DataGridTemplateColumn>
<DataControls:DataGridTextColumn
Binding="{Binding ShortDescription}"
Header="Short Description" Width="150" />
<DataControls:DataGridTextColumn
Binding="{Binding LongDescription}"
Header="Long Description" Width="*" />
</DataControls:DataGrid.Columns>
</DataControls:DataGrid>
</Grid>
</Controls:TabItem>
Столбец для родительской категории в сетке данных является полем с раскрывающимся списком (combobox), который использует список существующих категорий, чтобы можно было выбирать родительскую категорию по имени, а не запоминать ее идентификатор. Увы, Silverlight не по вкусу, когда в дерево визуальных элементов дважды загружается один и тот же объект. Поэтому мне пришлось объявить два источника данных предметной области: один — для сетки и другой — для просмотра поля с раскрывающимся списком. Кроме того, отделенный код (code-behind) для управления категориями получился довольно запутанным (рис. 3).
Рис. 3. Отделенный код для управления категориями
private void DomainDataSourceLoaded(object sender, LoadedDataEventArgs e)
{
if (e.HasError)
{
MessageBox.Show(e.Error.ToString(), "Load Error", MessageBoxButton.OK);
e.MarkErrorAsHandled();
}
}
private void SaveButtonClick(object sender, RoutedEventArgs e)
{
CategoryDomainDataSource.SubmitChanges();
}
private void CancelButtonClick(object sender, RoutedEventArgs e)
{
CategoryDomainDataSource.Load();
}
void ReloadChanges(object sender, SubmittedChangesEventArgs e)
{
CategoryDomainDataSource.Load();
}
Я не собираюсь излагать здесь полное пособие по MVVM — см. отличный трактат на эту тему в статье «WPF Apps with the Model-View-ViewModel Design Pattern» в февральском номере «MSDN Magazine» за 2009 г.(msdn.microsoft.com/magazine/dd419663). На рис. 4 показан один из способов применения MVVM в приложении RIA Services.
Рис. 4 Управление категориями через модель View
public CategoryManagementViewModel()
{
_dataContext = new CatalogContext();
LoadCategories();
}
private void LoadCategories()
{
IsLoading = true;
var loadOperation= _dataContext.Load(_dataContext.
GetCategoriesQuery());
loadOperation.Completed += FinishedLoading;
}
protected bool IsLoading
{
get { return _IsLoading; }
set
{
_IsLoading = value;
NotifyPropertyChanged("IsLoading");
}
}
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged!=null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
void FinishedLoading(object sender, EventArgs e)
{
IsLoading = false;
AvailableCategories=
new ObservableCollection<Category>(_dataContext.Categories);
}
public ObservableCollection<Category>AvailableCategories
{
get
{
return _AvailableCategories;
}
set
{
_AvailableCategories = value;
NotifyPropertyChanged("AvailableCategories");
}
}
Как видите, ViewModel отвечает за инициализацию контекста предметной области и уведомление UI в тот момент, когда происходит загрузка, а также за обработку запросов от UI на создание новых категорий, сохранение изменений в существующих категориях и повторную загрузку данных от сервиса предметной области. Это обеспечивает четкое разделение между UI и управляющей им логикой. Может показаться, что шаблон MVVM требует больше работы, но его изящество откроется, как только вам понадобится впервые изменить логику для передачи данных в UI. Кроме того, перенос процесса загрузки категорий в ViewModel позволяет значительно расчистить представление (как его XAML, так и отделенный код).
Рекомендация Используйте MVVM, чтобы избежать запутывания UI-кода сложной UI-логикой или, что еще хуже, запутывания вашей модели бизнес-объектов.
Транзакционный сценарий
Когда вы начнете расширять логику своего приложения, шаблон «формы поверх данных» станет громоздким. Поскольку логика обработки данных встраивается в UI (или в ViewModel, если вы сделали шаг вперед), она окажется разбросанной по всему приложению. Другой побочный эффект децентрализованной логики заключается в том, что другие разработчики могут не понять, что некая функциональность уже есть в приложении, что приведет к дублированию. Все это будет чистым кошмаром, когда понадобится изменить логику, так как изменения придется вносить во все места, где она присутствует (и то, если все эти места были документированы).
Шаблон транзакционных сценариев (стр. 110–115 в книге Фаулера) несколько облегчает эту ситуацию. Он позволяет отделить прикладную логику, управляющую данными, от UI.
Как определено Фаулером, транзакционный сценарий «организует бизнес-логику по процедурам, причем каждая процедура обрабатывает один запрос от презентационного уровня». Транзакционные сценарии — нечто гораздо большее простых CRUD-операций. По сути, они размещаются перед шлюзом к табличным данным для обработки CRUD-операций. В предельном случае отдельный транзакционный сценарий обрабатывал бы каждое чтение и запись в базу данных. Но мы же разумные люди и всему знаем меру.
Транзакционный сценарий полезен, когда ваше приложение должно координировать взаимодействие между двумя сущностями, например при создании сопоставления между двумя экземплярами разных классов сущностей. Например, в системе управления каталогами я обозначаю продукт, доступный для заказа бизнес-подразделением, созданием записи в каталоге. Запись идентифицирует продукт, бизнес-подразделение, артикул и срок, в течение которого он может быть заказан как внутри предприятия, так и извне. Чтобы упростить формирование записей в каталоге, я создал метод в сервисе предметной области (см. следующий фрагмент кода), который предоставляет транзакционный сценарий для изменения доступности продукта бизнес-подразделению; при этом в UI не требуется напрямую манипулировать записями в каталоге.
Фактически сервис предметной области даже не открывает доступа к записям в каталоге:
public void CatalogProductForBusinessUnit(Product product, int businessUnitId)
{
var entry = ObjectContext.CreateObject<CatalogEntry>();
entry.BusinessUnitId = businessUnitId;
entry.ProductId = product.Id;
entry.DateAdded = DateTime.Now;
ObjectContext.CatalogEntries.AddObject(entry);
ObjectContext.SaveChanges();
}
Вместо того чтобы предоставлять его как функцию в контексте доменной области на клиентской стороне, RIA Services генерируют функцию в сущности (в данном случае — Product), которая при вызове помещает уведомление об изменении в объект, интерпретируемый на серверной стороне как вызов метода из сервиса предметной области.
Фаулер рекомендует для подхода к реализации транзакционного сценария:
- • с помощью объектов команд, которые инкапсулируют операции и которые при необходимости можно обходить;
- • на основе единственного класса, хранящего набор транзакционных сценариев.
Здесь я выбрал второй подход, но ничто не мешает вам использовать команды. Отсутствие прямого доступа к записи в каталоге из UI-уровня делает транзакционный сценарий единственным средством создания записей в каталоге. Если вы используете шаблон команд, это правило вводится в действие по соглашению. Если разработчик забудет о существовании какой-либо команды, все кончится там, откуда начиналось, — в фрагментации и дублировании логики.
Другое преимущество от размещения транзакционного сценария в сервисе предметной области состоит в том, что логика выполняется на серверной стороне (как я уже упоминал). Если вы не хотите раскрывать алгоритмы или если вам нужно исключить попытки злонамеренной модификации ваших данных, такой путь как раз для вас.
Рекомендация Используйте транзакционный сценарий, если ваша бизнес-логика становится слишком сложной для «форм поверх данных» или если вы хотите выполнять логику на серверной стороне (или и то, и другое).
Бизнес-логика и UI-логика Я уже несколько раз ссылался на UI- и бизнес-логику, и, хотя разница между ними поначалу может показаться незначительной, она важна. UI-логика отвечает за презентационный уровень:что показывать на экране и как (например, элементы, используемые для заполнения поля со списком). С другой стороны, бизнес-логика — это то, что управляет самим приложением (скажем, определяет скидку на покупку через Интернет). Оба вида логики — важные грани приложения, и, если допускается их смешение, появляется другой шаблон — см. статью Брайена Фута (Brian Foote) и Джозефа Йодера (Joseph Yoder) «Big Ball of Mud» (laputan.org/mud).
Передача нескольких сущностей сервису предметной области Вы можете передавать методу собственного сервиса предметной области только одну сущность. Например, метод:
public void CatalogProductForBusinessUnit(Product product, int businessUnitId)
не будет работать, если вы попытаетесь использовать следующую сигнатуру вместо предыдущей:
public void CatalogProductForBusinessUnit(Product product, BusinessUnit bu)
RIA Services не станут генерировать клиентский прокси для этой функции, потому что… ну, в общем, таковы правила. Метод сервиса принимает только одну сущность. В большинстве случаев это не создает никаких проблем:если есть сущность, есть и ее ключ, и вы можете неоднократно считывать ее.
Но допустим просто для демонстрации, что считывание сущности — операция дорогостоящая (возможно, она находится по другую сторону веб-сервиса). Тогда можно сообщить сервису предметной области, чтобы он сохранял копию данной сущности, как показано ниже:
public void StoreBusinessUnit(BusinessUnit bu)
{
HttpContext.Current.Session[bu.GetType().FullName+bu.Id] = bu;
}
public void CatalogProductForBusinessUnit(Product product, int businessUnitId)
{
var currentBu = (BusinessUnit)HttpContext.Current.
Session[typeof(BusinessUnit).FullName + businessUnitId];
// Use the retrieved BusinessUnit Here.
}
Так как сервис предметной области выполняется в ASP.NET, он имеет полный доступ к сеансу и кешу ASP.NET; это полезно, если вам нужно автоматически удалять объект из памяти по истечении определенного времени. Я использую такой прием в одном из своих проектов, где мне приходится получать CRM-данные от нескольких удаленных веб-сервисов и представлять их пользователю в унифицированном UI. Я применяю явный метод, поскольку какие-то данные стоит кешировать, а какие-то — нет.
Модель предметной области
Иногда бизнес-логика становится настолько сложной, что даже транзакционные сценарии не позволяют управлять ею должным образом. Зачастую такая ситуация проявляется тем, что в транзакционном сценарии содержится логика со сложным ветвлением, или тем, что у вас появляется множество транзакционных сценариев для учета различных нюансов в логике. Другой признак — в приложении появляется все больше транзакционных сценариев и их требуется часто обновлять для соответствия быстро меняющимся требованиям бизнеса.
Если вы заметили любой из этих симптомов, пора подумать о полнофункциональной модели предметной области (с. 116-124 в книге Фаулера). У рассмотренных до сих пор шаблонов было одно общее: сущности — нечто большее, чем DTO-объекты, которые не содержат логики (некоторые считают это антишаблоном и называют его Anemic Domain Model). Одно из основных преимуществ объектно-ориентированной разработки — возможность инкапсулировать данные и связанную с ними логику. Полнофункциональная модель предметной области (rich domain model) использует это преимущество и позволяет поместить логику в сущность.
Детали разработки модели предметной области выходят за рамки этой статьи. Отличное описание этой проблематики см. в книге Эрика Эванса (Eric Evans) «Domain-Driven Design: Tackling Complexity in the Heart of Software» (Addison-Wesley, 2004) или в уже упомянутой книге Коуда по объектным моделям. Однако я могу проиллюстрировать, как модель предметной области помогает справляться с некоторыми из проблем.
Допустим, часть клиентов KharaPOS хочет иметь возможность просматривать хронологию продаж определенных линеек продуктов и с учетом рыночной конъюнктуры решать, что делать с данными линейками товаров — расширять (добавлять товары в линейки), сокращать, полностью сворачивать их или оставить неизменными на текущий сезон.
Данные о продажах уже имеются в другой подсистеме KharaPOS, и в данном случае все, что нужно, есть в системе каталогов. Я просто добавлю в нашу модель сущностей данных представление продаж товаров (только для чтения), как показано на рис. 5.
Рис. 5. Entity Data Model, обновленный с Sales Data
Теперь остается добавить логику выборки товаров в модель предметной области. Поскольку я выбираю товары для рынка, я помещу логику в класс BusinessUnit (использование частичного класса с расширением shared.cs или shared.vb сообщает RIA Services, что вы хотите, чтобы он мог работать на клиенте). Исходный код показан на рис. 6.
Рис. 6. Логика домена для выбора продуктов для бизнес-подразделения
public partial class BusinessUnit
{
public void SelectSeasonalProductsForBusinessUnit(
DateTime seasonStart, DateTime seasonEnd)
{
// Get the total sales for the season
var totalSales = (from sale in Sales
where sale.DateOfSale > seasonStart
&& sale.DateOfSale < seasonEnd
select sale.LineItems.Sum(line => line.Cost)).
Sum(total=>total);
// Get the manufacturers for the business unit
var manufacturers =
Catalogs.Select(c =>c.Product.ManuFacturer).
Distinct(new Equality<ManuFacturer>(i => i.Id));
// Group the sales by manufacturer
var salesByManufacturer =
(from sale in Sales
where sale.DateOfSale > seasonStart
&& sale.DateOfSale < seasonEnd
from lineitem in sale.LineItems
join manufacturer in manufacturers on
lineitem.Product.ManufacturerId equals manuFacturer.Id
select new
{
Manfacturer = manuFacturer,
Amount = lineitem.Cost
}).GroupBy(i => i.Manfacturer);
foreach (var group in salesByManufacturer)
{
var manufacturer = group.Key;
var pct = group.Sum(t => t.Amount)/totalSales;
SelectCatalogItemsBasedOnPercentage(manufacturer, pct);
}
}
private void SelectCatalogItemsBasedOnPercentage(
ManuFacturer manufacturer, decimal pct)
{
// Rest of logic here.
}
}
Выполнить автоматический выбор товаров за сезон так же просто, как вызвать новую функцию в BusinessUnit, а потом функцию SubmitChanges в DomainContext. В будущем, если в этой логике будет обнаружена ошибка или потребуется обновление логики, я буду точно знать, где ее искать. Я не только централизовал логику, но и сделал объектную модель более выразительной. Почему это хорошо, объясняется на с. 246 книги Эванса:
Если разработчик для использования компонента должен учитывать его реализацию, всякий смысл инкапсуляции пропадает. Если новому разработчику приходится догадываться о предназначении объекта или операции по реализации, то о задачах, выполняемых этим классом или операцией, он может догадаться лишь случайно. Код может проработать какое-то время, но концептуальная основа окажется искаженной, и два разработчика (новый и изначальный) будут двигаться в противоположных направлениях.
Если перефразировать, то, явно выразив в имени функции ее предназначение и инкапсулировав логику (наряду с несколькими комментариями, поясняющими, что именно делается), я упростил следующему разработчику (даже если таковым буду я сам месяцев эдак через пять) понимание того, что выполняется этой функцией, даже не глядя на ее реализацию. Размещение этой логики вместе с данными, к которым она относится, позволяет задействовать выразительные средства объектно-ориентированных языков.
Рекомендация Используйте модель предметной области, когда ваша логика становится сложной и может охватывать сразу несколько сущностей. Инкапсулируйте логику в объект, с которым она имеет наибольшее сродство, и присвойте его операции описательное имя.
Разница между моделью предметной области и транзакционным сценарием в RIA Services Вероятно, вы заметили, что и в транзакционном сценарии, и в модели предметной области вызов адресуется непосредственно сущности. Однако логика в этих двух шаблонах размещается в разных местах. В случае транзакционного сценария вызов функции применительно к сущности просто указывает контексту/сервису предметной области, что соответствующую функцию нужно вызвать в сервисе предметной области при следующей передаче изменений. В случае модели предметной области логика выполняется на клиентской стороне, а при передаче изменения фиксируются.
Хранилище и объекты запросов Сервис предметной области естественным образом реализует шаблон хранилища (repository pattern) (см. с. 322 в книге Фаулера). В WCF RIA Services Code Gallery (code.msdn.microsoft.com/RiaServices) группа RIA Services предлагает отличный пример создания явной реализации этого шаблона поверх DomainContext. Это расширяет возможности тестирования вашего приложения без обращения к уровню сервисов или базе данных. Кроме того, в моем блоге (azurecoding.net/blogs/brownie) есть реализация шаблона объекта запроса (Фаулер, с. 316) поверх хранилища, при котором выполнение запроса на серверной стороне откладывается до тех пор, пока не происходит реальное перечисление.
Уровень прикладных сервисов
Вопрос навскидку: как вы поступаете в том случае, когда нужно использовать полнофункциональную модель предметной области, но раскрывать ее логику на UI-уровне крайне нежелательно? Вот здесь и пригодится шаблон уровня прикладных сервисов (application service layer pattern) (Фаулер, с. 133). Если у вас есть модель предметной области, этот шаблон легко реализовать переносом логики предметной области из shared.cs в отдельный частичный класс и помещением функции в сервис предметной области, который запускает функцию применительно к сущности.
Уровень прикладных сервисов действует как упрощенный фасад вашей модели предметной области, предоставляя операции, но не их детали. Другое преимущество в том, что ваши объекты предметной области смогут принимать внутренние зависимости, не требуя того же от клиентов уровня прикладных сервисов. В некоторых случаях (см. пример на рис. 6) сервис предметной области выдает один простой вызов, адресованный некой сущности. Иногда он может управлять несколькими сущностями, но будьте осторожны:слишком большой объем операций управления отбросит вас к транзакционному сценарию, и преимущества инкапсуляции логики в самой сущности будут утрачены.
Рекомендация Используйте уровень прикладных сервисов для создания простого фасада модели предметной области и исключите зависимость UI-уровня от ваших сущностей.
Бонус: разграниченный контекст
На форумах RIA участники часто спрашивают:«Как распределить очень большую базу данных между сервисами предметных областей, чтобы ею было легче управлять?». А следом такой вопрос:«как обрабатывать сущности, которые должны присутствовать в нескольких сервисах предметных областей?». Поначалу я считал, что в таких вещах вообще нет нужды; сервис предметной области должен выступать в роли уровня сервисов поверх вашей модели предметной области, и единственный сервис предметной области должен служить фасадом для всей вашей предметной области.
Однако при подготовке этой статьи я наткнулся на шаблон разграниченного контекста (bounded-context pattern) (Эванс, с. 336), о котором я читал раньше, но забыл к тому моменту, когда отвечал на перечисленные выше вопросы. Основное достоинство этого шаблона в том, что в любых крупных проектах используется несколько предметных подобластей. Возьмите для примера KharaPOS, где есть одна модель предметной области для каталогов и другая — для продаж.
Разграниченный контекст позволяет мирно сосуществовать этим моделям предметных областей, даже если у них есть общие элементы (вроде Sale, Business Unit, Product и LineItem, которые присутствуют как в подсистеме продаж, так и в каталоге). К сущностям применяются разные правила в зависимости от того, из какой модели предметной области с ними взаимодействуют (Sale и LineItem предназначены только для чтения в каталоге). Суть в том, что операции никогда не выходят за границы своих контекстов. Это облегчает жизнь, так как Silverlight не поддерживает транзакции, охватывающие несколько сервисов предметных областей.
Рекомендация Используйте разграниченные контексты для разбиения крупной системы на логические подсистемы.
«Яма успеха»
В этой статье мы увидели, что RIA Services поддерживают основные корпоративные шаблоны и требуют при этом минимальных усилий с вашей стороны. Редко, когда инфраструктуры оказываются столь простыми в использовании и в то же время достаточно гибкими для поддержки хоть самых элементарных приложений для ввода данных в электронные таблицы, хоть самых сложных бизнес-приложений, да вдобавок не требуют каких-то радикальных усилий при переходе от одного конца спектра к другому. Это настоящая «яма успеха» («Pit of Success»), о которой упоминал Брэд Адамс (Brad Abrams) в одноименной статье в своем блоге (blogs.msdn.com/brada/archive/2003/10/02/50420.aspx).