- •Оглавление
- •Предисловие
- •Введение
- •Часть I Обзор Глава 1 "Расслоение" системы
- •Развитие модели слоев в корпоративных программных приложениях
- •Три основных слоя
- •Где должны функционировать слои
- •Глава 2 Организация бизнес-логики
- •Выбор типового решения
- •Глава 3 Объектные модели и реляционные базы данных
- •Архитектурные решения
- •Функциональные проблемы
- •Считывание данных
- •Взаимное отображение объектов и реляционных структур
- •Отображение связей
- •Наследование
- •Реализация отображения
- •Двойное отображение
- •Использование метаданных
- •Соединение с базой данных
- •Другие проблемы
- •Дополнительные источники информации
- •Глава 4 Представление данных в Web
- •Типовые решения представлений
- •Типовые решения входных контроллеров
- •Дополнительные источники информации
- •Глава 5 Управление параллельными заданиями
- •Проблемы параллелизма
- •Контексты выполнения
- •Изолированность и устойчивость данных
- •Стратегии блокирования
- •Предотвращение возможности несогласованного чтения данных
- •Разрешение взаимоблокировок
- •Транзакции
- •Типовые решения задачи обеспечения автономного параллелизма
- •Параллельные операции и серверы приложений
- •Дополнительные источники информации
- •Глава 6 Сеансы и состояния
- •В чем преимущество отсутствия "состояния"
- •Состояние сеанса
- •Глава 7 Стратегии распределенных вычислений
- •Соблазны модели распределенных объектов
- •Интерфейсы локального и удаленного вызова
- •Когда без распределения не обойтись
- •Сужение границ распределения
- •Интерфейсы распределения
- •Глава 8 Общая картина
- •Предметная область
- •Источник данных
- •Платформы и инструменты
- •Другие модели слоев
- •Часть II Типовые решения Глава 9 Представление бизнес-логики Сценарий транзакции (Transaction Script)
- •Модель предметной области (Domain Model)
- •Модуль таблицы (Table Module)
- •Слой служб (Service Layer)
- •Глава 10 Архитектурные типовые решения источников данных Шлюз таблицы данных (Table Data Gateway)
- •Шлюз записи данных (Row Data Gateway)
- •Активная запись (Active Record)
- •Преобразователь данных (Data Mapper)
- •Глава 11 Объектно-реляционные типовые решения, предназначенные для моделирования поведения Единица работы (Unit of Work)
- •Коллекция объектов (Identity Map)
- •Загрузка по требованию (Lazy Load)
- •Глава 12 Объектно-реляционные типовые решения, предназначенные для моделирования структуры Поле идентификации (Identity Field)
- •Отображение внешних ключей (Foreign Key Mapping)
- •Отображение с помощью таблицы ассоциаций (Association Table Mapping)
- •Отображение зависимых объектов (Dependent Mapping)
- •Внедренное значение (Embedded Value)
- •Сериализованный крупный объект (Serialized lob)
- •Наследование с одной таблицей (Single Table Inheritance)
- •Наследование с таблицами для каждого класса (Class Table Inheritance)
- •Наследование с таблицами для каждого конкретного класса (Concrete Table Inheritance)
- •Преобразователи наследования (Inheritance Mappers)
- •Глава 13 Типовые решения объектно-реляционного отображения с использованием метаданных Отображение метаданных (Metadata Mapping)
- •Объект запроса (Query Object)
- •Хранилище (Repository)
- •Глава 14 Типовые решения, предназначенные для представления данных в Web Модель-представление-контроллер (Model View Controller)
- •Контроллер страниц (Page Controller)
- •Контроллер запросов (Front Controller)
- •Представление по шаблону (Template View)
- •Представление с преобразованием (Transform View)
- •Двухэтапное представление (Two Step View)
- •Контроллер приложения (Application Controller)
- •Глава 15 Типовые решения распределенной обработки данных Интерфейс удаленного доступа (Remote Facade)
- •Объект переноса данных (Data Transfer Object)
- •Глава 16 Типовые решения для обработки задач автономного параллелизма Оптимистическая автономная блокировка (Optimistic Offline Lock)
- •Пессимистическая автономная блокировка (Pessimistic Offline Lock)
- •Блокировка с низкой степенью детализации (Coarse-Grained Lock)
- •Неявная блокировка (Implicit Lock)
- •Глава 17 Типовые решения для хранения состояния сеанса Сохранение состояния сеанса на стороне клиента (Client Session State)
- •Сохранение состояния сеанса на стороне сервера (Server Session State)
- •Сохранение состояния сеанса в базе данных (Database Session State)
- •Глава 18 Базовые типовые решения Шлюз (Gateway)
- •Преобразователь (Mapper)
- •Супертип слоя (Layer Supertype)
- •Отделенный интерфейс (Separated Interface)
- •Реестр (Registry)
- •Объект-значение (Value Object)
- •Деньги (Money)
- •Частный случай (Special Case)
- •Дополнительный модуль (Plugin)
- •Фиктивная служба (Service Stub)
- •Множество записей (Record Set)
- •Список типовых решений
- •Шпаргалка
- •Как управлять сложным потоком функций приложения?
- •Как взаимодействовать с базой данных?
- •Как избежать загрузки в оперативную память всего содержимого базы данных?
- •Как сохранить структуры наследования в реляционной базе данных?
Модуль таблицы (Table Module)
Объект, охватывающий логику обработки всех записей хранимой или виртуальной таблицы базы данных
Одна из ключевых предпосылок объектной модели — сочетание элементов данных и пользующихся ими функций. Традиционный объектно-ориентированный подход основан на концепции объектов с идентификационными признаками в совокупности с требованиями модели предметной области (Domain Model). Если, например, речь идет о классе, представляющем сущность "служащий", любой экземпляр класса соответствует определенному служащему; коль скоро есть ссылка на объект, отвечающий служащему, с ним легко выполнять все необходимые операции, собирать информацию и следовать в направлении связей с другими объектами.
Одна из проблем модели предметной области заключается в сложности создания интерфейсов к реляционным базам данных; последние в подобной ситуации приобретают роль эдаких бедных родственников, с которыми никто не желает иметь дела. Поэтому считывание информации из базы данных и запись ее с необходимыми преобразованиями превращается в прихотливую игру ума.
Типовое решение модуль таблицы предусматривает создание по одному классу на каждую таблицу базы данных, и единственный экземпляр класса содержит всю логику обработки данных таблицы. Основное отличие модуля таблицы от модели предметной области состоит в том, что если, например, приложение обслуживает множество заказов, в соответствии с моделью предметной области придется сконструировать по одному объекту на каждый заказ, а при использовании модуля таблицы понадобится всего один объект, представляющий одновременно все заказы.
Принцип действия
Сильная сторона решения модуль таблицы заключается в том, что оно позволяет сочетать данные и функции для их обработки и в то же время эффективно использовать ресурсы реляционной базы данных. На первый взгляд модуль таблицы во многом напоминает обычный объект, но отличается тем, что не содержит какого бы то ни было упоминания об идентификационном признаке объекта. Если, скажем, требуется получить адрес служащего, для этого применяется метод anEmployeeModule.getAddress(long employeeID). В том случае, когда необходимо выполнить операцию, касающуюся определенного служащего, соответствующему методу следует передать ссылку на идентификатор, значение которого зачастую совпадает с первичным ключом служащего в таблице базы данных.
Модулю таблицы, как правило, отвечает некая табличная структура данных. Подобная информация обычно является результатом выполнения SQL-запроса и сохраняется в виде множества записей (Record Set). Модуль таблицы предоставляет обширный арсенал методов ее обработки. Объединение функций и данных обеспечивает многие преимущества модели инкапсуляции.
Нередко для решения общей задачи необходимо создать несколько модулей таблицы и, более того, позволить им манипулировать одним и тем же множеством записей (рис. 9.4).
Наиболее очевидный пример связан с использованием отдельных модулей таблицы для каждой (хранимой) таблицы базы данных. Кроме того, можно сконструировать модули таблицы для любых достойных SQL-запросов и виртуальных таблиц.
Конкретный модуль таблицы может принимать вид экземпляра класса или набора статических методов. Достоинство первого варианта состоит в том, что он позволяет инициировать модуль данными существующего множества записей, чаще всего получаемого в результате обработки SQL-запроса. Для манипуляции записями применяются методы класса. Не исключается и возможность создания иерархии наследования, когда, скажем, определяются базовый класс с описанием контракта общего вида и целое семейство производных классов, отвечающих частным разновидностям контрактов.
Рис. 9.4. Несколько модулей таблицы способны обращаться к одному и тому же множеству записей
Модуль таблицы способен содержать методы-оболочки, представляющие запросы к базе данных. Альтернативой служит шлюз таблицы данных (Table Data Gateway). Недостаток последнего обусловлен необходимостью конструирования дополнительного класса, а преимущество заключается в возможности применения единого модуля таблицы для данных из различных источников, поскольку каждому отвечает собственный шлюз таблицы данных.
Шлюз таблицы данных позволяет структурировать информацию в виде множества записей, которое затем передается конструктору модуля таблицы в качестве аргумента (рис. 9.5). Если необходимо использовать несколько модулей таблицы, все они могут быть созданы на основе одного и того же множества записей. Затем каждый модуль таблицы применяет к множеству записей функции бизнес-логики и передает измененное множество записей слою представления для отображения и редактирования информации средствами графических табличных элементов управления. Последние не "осведомлены", откуда поступили данные — непосредственно от реляционной СУБД или от промежуточного модуля таблицы, который успел осуществить их предварительную обработку. По завершении редактирования информация возвращается модулю таблицы для проверки перед сохранением в базе данных. Одно из преимуществ подобного стиля — возможность тестирования модуля таблицы путем "искусственного" создания множества записей в памяти без обращения к реальной таблице базы данных.
Слово "таблица" в названии типового решения подчеркивает, что в приложении предусматривается по одному модулю таблицы для каждой хранимой таблицы базы данных. Это правда, но не вся. Полезно также иметь модули таблицы для общеупотребительных виртуальных таблиц и запросов, поскольку структура модуля таблицы на самом деле не зависит напрямую от структуры физических таблиц базы данных, а определяется в основном характеристиками виртуальных таблиц и запросов, используемых в приложении.
Рис. 9.5. Схема взаимодействия слоев кода с модулем таблицы
Назначение
Типовое решение модуль таблицы во многом основывается на табличной структуре данных и потому допускает очевидное применение в ситуациях, где доступ к информации обеспечивается при посредничестве множеств записей. Структуре данных отводится центральная роль, так что методы обращения к данным в структуре должны быть прямолинейными и эффективными.
Модуль таблицы, однако, не позволяет воспользоваться всей мощью объектного подхода к организации сложной бизнес-логики. Вам не удастся создать прямые связи от объекта к объекту, да и механизм полиморфизма действует в этих условиях не безукоризненно. Поэтому для реализации особо изощренной логики предпочтительнее модель предметной области. По существу, проблема сводится к поиску компромисса между способностью модели предметной области к эффективному отображению бизнес-логики и простотой интеграции кода приложения и реляционных структур данных, обеспечиваемой модулем таблицы.
Если объекты модели предметной области относительно точно отвечают таблицам базы данных, возможно, целесообразнее применить модель предметной области совместно с активной записью (Active Record). Модуль таблицы, однако, ведет себя лучше, чем комбинация модели предметной области и активной записи, если разные части приложения основаны на общей табличной структуре данных. Впрочем, в среде Java, например, модуль таблицы пока не пользуется популярностью, хотя с распространением модели множеств записей ситуация, возможно, и изменится.
Чаще всего образцы использования модуля таблицы приходится видеть в проектах на основе архитектуры Microsoft COM. В технологии СОМ (и .NET) множество записей представляет собой основное хранилище данных, с которыми оперирует приложение. Множества записей могут передаваться фактическим элементам управления для воспроизведения информации на экране. Добротный механизм доступа к реляционным данным, представленным в виде множеств записей, реализован в семействе библиотек Microsoft ADO. В подобных случаях модуль таблицы позволяет описать бизнес-логику в хорошо структурированном виде.
Пример: определение зачтенного дохода (С#)
Настал черед вновь обратиться к задаче определения зачтенного дохода (см. раздел 9.1.3), но на сей раз в контексте модуля таблицы. Напомню, что цель состоит в вычислении зачтенного дохода ("revenue recognitions") по контракту ("contract") в зависимости от упомянутого в нем типа продукта ("product"). Здесь будем различать три типа продуктов — текстовый процессор (ниже в примере кода будем ссылаться на него с помощью значения wp (от word processor) перечислимого типа ProductType), электронная таблица (ss — spreadsheet) и СУБД (db — database).
Модуль таблицы основывается на некой структуре данных, обычно реляционной (в будущем, вероятно, на эту роль сможет претендовать модель XML). В нашем случае реляционную схему допустимо изобразить так, как показано на рис. 9.6.
Рис. 9.6. Схема базы данных о контрактах, продуктах и зачтенном доходе
Классы, манипулирующие этими данными, следуют единой форме: каждой таблице отвечает определенный класс модуля таблицы. В архитектуре .NET можно создать объект множества данных, обеспечивающий представление структуры базы данных в памяти. Поэтому имеет смысл конструировать классы, способные оперировать множеством данных. Каждый класс модуля таблицы содержит элемент данных системного .NET-типа DataTable, который соответствует одной из таблиц множества данных. Способность считывать табличную информацию свойственна всем модулям таблицы, ею может обладать даже супертип слоя (Layer Supertype).
class TableModule...
protected DataTable table;
protected TableModule(DataSet ds, String tableName) {
table = ds.Tables[tableName];
}
Конструктор производного класса вызывает конструктор суперкласса и передает наименование конкретной таблицы.
class Contract...
public Contract (DataSet ds) : base (ds, "Contracts") {}
Это позволяет создать новый модуль таблицы, просто передав множество данных соответствующему конструктору, синтаксис которого приведен ниже, и отделить код, конструирующий множество данных, от модулей таблицы, что отвечает рекомендациям спецификации ADO.NET.
contract = new Contract(dataset);
Очень удобен инструмент индексации, реализованный в С#, который позволяет "добраться" до определенной записи таблицы данных по значению первичного ключа.
class Contract...
public DataRow this [long key] {
get {
String filter = String.Format("ID = {0}", key);
return table.Select(filter)[0];
}
}
Функции первой группы осуществляют вычисление значений зачтенного дохода по контракту и обновление соответствующей таблицы. Получаемые значения зависят от типа продукта. Поскольку эти операции основаны преимущественно на данных из таблицы контрактов, я решил добавить в класс Contract надлежащий метод.
class Contract...
public void CalculateRecognitions (long contractID) {
DataRow contractRow = this[contractID];
Decimal amount = (Decimal)contractRow["amount"];
RevenueRecognition rr = new RevenueRecognition (table.DataSet);
Product prod = new Product(table.DataSet);
long prodID = GetProductId(contractID);
if (prod.GetProductType(prodID) == ProductType.WP) {
rr.Insert(contractID, amount, (DateTime) GetWhenSigned(contractID));
}else if (prod.GetProductType(prodID) == ProductType.SS) {
Decimal[] allocation = allocate(amount,3);
rr.Insert(contractID, allocation[0], (DateTime) GetWhenSigned(contractID));
rr.Insert(contractID, allocation[1], (DateTime) GetWhenSigned(contractID).
AddDays(60));
rr.Insert(contractID, allocation[2], (DateTime) GetWhenSigned(contractID).
AddDays(90));
}else if (prod.GetProductType(prodID) == ProductType.DB) {
Decimal[] allocation = allocate(amount,3);
rr.Insert(contractID, allocation[0], (DateTime) GetWhenSigned(contractID));
rr.Insert(contractID, allocation[1], (DateTime) GetWhenSigned(contractID).
AddDays(30));
rr.Insert(contractID, allocation[2], (DateTime) GetWhenSigned(contractID).
AddDays(60));
}else throw new Exception("invalid product id");
}
private Decimal[] allocate(Decimal amount, int by) {
Decimal lowResult = amount / by;
lowResult = Decimal.Round(lowResult,2);
Decimal highResult = lowResult + 0.01m;
Decimal[] results = new Decimal[by];
int remainder = (int) amount % by;
for (int i = 0; i < remainder; i++) results[i] = highResult;
for (int i = remainder; i < by; i++) results[i] = lowResult;
return results;
}
Здесь можно было бы применить типовое решение деньги (Money), но для разнообразия я показал, как работать с типом Decimal. Метод allocate схож с тем, который используется в контексте решения деньги.
Другие классы также необходимо снабдить соответствующими методами. Класс продуктов должен "уметь" сообщать о типе конкретного продукта. Этого можно добиться посредством соответствующих перечислимого типа и метода.
public enum ProductType {WP, SS, DB};
class Product...
public ProductType GetProductType (long id) {
String typeCode = (String) this[id]["type"];
return (ProductType) Enum.Parse(typeof(ProductType), typeCode);
}
Метод GetProductType инкапсулирует информацию таблицы типа DataTable. Имеет смысл обеспечить инкапсуляцию всех столбцов данных, а не обращаться к ним напрямую, как я поступал с полем amount таблицы контрактов (хотя использовать модель инкапсуляции, вообще говоря, неплохо, в данном случае я от нее отказался, поскольку она вступала в противоречие с предположением о том, что различные части системы способны адресовать множество данных непосредственно). Использовать отдельные функции доступа к столбцам имеет смысл только тогда, когда необходимо выполнять дополнительные операции (например, преобразование строки в значение типа ProductType).
Здесь уместно напомнить и о том, что предпочтительнее пользоваться строго типизированными множествами данных .NET, хотя в этом случае я применил нетипизированное множество.
Еще одна функция связана со вставкой новой записи, описывающей зачтенный доход.
class RevenueRecognition...
public long Insert (long contractID, Decimal amount, DateTime date) {
DataRow newRow = table.NewRow();
long id = GetNextID();
newRow["ID"] = id;
newRow["contractID"] = contractID;
newRow["amount"] = amount;
newRow["date"]= String.Format("{0:s}", date);
table.Rows.Add(newRow);
return id;
}
И вновь значение метода не столько в инкапсуляции записи данных, сколько в том, что он структурирует строки кода, позволяя избежать их неоднократного повторения.
Функции второй группы связаны с суммированием значений всех зачтенных доходов по контракту, полученных на определенную дату. Поскольку вычисления затрагивают таблицу зачтенного дохода, целесообразно определить подходящий метод в классе, отвечающем этой таблице.
class RevenueRecognition...
public Decimal RecognizedRevenue (long contractID, DateTime asOf) {
String filter = String.Format("ContractID = {0}AND date <= #{1:d}#", contractID,
asOf);
DataRow[] rows = table.Select(filter);
Decimal result = 0m;
foreach (DataRow row in rows) {
result += (Decimal)row["amount"];
}
return result;
}
Этот фрагмент демонстрирует прекрасное средство ADO.NET, позволяющее определять строку предложения where и выбирать необходимое подмножество записей таблицы типа DataTable. Ничто не мешает пойти еще дальше и воспользоваться агрегирующей функцией.
class RevenueRecognition...
public Decimal RecognizedRevenue2 (long contractID, DateTime asOf) {
String filter = String.Format("ContractID = {0}AND date <= #{1:d}#", contractID,
asOf);
String computeExpression = "sum(amount)";
Object sum = table.Compute(computeExpression, filter);
return (sum is System.DBNull) ? 0 : (Decimal) sum;
}
