Table Module – Domain Logic Patterns (PoEAA)

Один из основных принципов в ООП - сочетание данных и методов обработки этих данных. Традиционный подход основан на объектах с соответствием, как, например, в паттерне Domain Model. Таким образом, если есть класс Employee, люой экземпляр этого класса соответствует конкретному работнику. Эта структура работает хорошо, потому что, имея связь, можно выполнять операции, использователь отношения, и собирать данные о работнике.
Одна из проблем в паттерне Domain Model заключается в интерфейсе к БД. Этот подход относится к БД, как к сумашедшей тётушке, запертой на чердаке, с которой никто не хочет говорить. Частенько, приходится сильно постараться, чтобы записать или считать данные из БД, преобразуя их между двумя представлениями.

Паттерн Table Module разделяет логику области определения (домена) на отдельные классы для каждой таблицы в БД и один экземпляр класса содержит различные процедуры, работающие с данными. Основное отличие от Domain Model заключается в том, что если есть несколько заказов, то Domain Model будет создавать для каждого из заказов свой объект, а Table Module будет управлять всем заказами при помощи одного объекта.
Как это работает:
Преимущество табличного модуля заключается в том, что он позволяет объединять данные и поведение вместе и одновременно использовать преимущества реляционной базы данных. На поверхности Table Table очень похож на обычный объект. Ключевое отличие состоит в том, что у него нет понятия идентичности для объектов, с которыми он работает. Таким образом, если вы хотите получить адрес сотрудника, вы используете метод, такой как anEmployeeModule.getAddress(long employeeID). Каждый раз, когда вы хотите что-то сделать с конкретным сотрудником, вы должны передать какой-то id. Часто это будет первичный ключ, используемый в базе данных.
Обычно Table Module используютс резервной структурой данных, ориентированной на таблицы. Табличные данные обычно являются результатом вызова SQL и содержатся в наборе записей, который имитирует таблицу SQL. Модуль Table предоставляет вам явный интерфейс на основе методов, который воздействует на эти данные. Группировка поведения с таблицей дает вам много преимуществ инкапсуляции в том, что поведение близко к данным, с которыми она будет работать.
Table Module может быть экземпляром или коллекцией статических методов. Преимущество экземпляра в том, что он позволяет инициализировать Table Module с существующим набором (результатом запроса). Затем вы можете использовать экземпляр для управления строками в наборе записей. Экземпляры также позволяют использовать наследование, поэтому мы можем написать модуль срочного контракта, который содержит дополнительное поведение к обычному контракту. Table Module может включать запросы как фабричные методы. Альтернативой является Table Data Gateway.
Когда использовать Table Module?
Табличный модуль в значительной степени основан на данных, ориентированных на таблицы, поэтому очевидно, что его использование имеет смысл при доступе к табличным данным с использованием Record Set. Это размещает структуру данных в центр системы, для того чтобы доступ к структуре данных был достаточно простым.
Тем не менее, Table Module не дает вам полной гибкости объектов в организации сложной логики. Вы не сможете иметь прямые отношения между экземплярами, и полиморфизм не работает так хорошо как хотелось бы. Таким образом, для обработки сложной доменной логики Domain Model является лучшим выбором. Самый хороший пример где мы можем столкнуться с этим шаблоном - это разработки Microsoft COM. В COM (и .NET)Record Set является основным хранилищем данных в приложении. Record Set'ы могут быть переданы в пользовательский интерфейс, где виджеты с данными отображают информацию. Библиотеки ADO от Microsoft предоставляют вам хороший механизм доступа к реляционным данным в виде наборов записей. В этой ситуации Table Module позволяет вам отлично организовать бизнес-логику в приложении, не теряя при этом работу различных элементов над табличными данными.
Пример реализации Table Module на C#
Реализация класса TableModule:
class TableModule...
protected DataTable table;
protected TableModule(DataSet ds, String tableName)
{
table = ds.Tables[tableName];
}
Реализация класса Контракт:
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;
}
Продукт должен быть в состоянии сказать нам, какой это тип. Для этого мы добавляем enum для типа продукта и метода поиска.
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 инкапсулирует данные в таблице данных. Есть аргумент для этого для всех столбцов данных, в отличие от прямого доступа к ним, как я сделал с суммой в контракте. Хотя инкапсуляция обычно полезна, я не использую ее здесь, поскольку она не соответствует предположению о том, что различные части системы имеют прямой доступ к набору данных. Инкапсуляция отсутствует, когда набор данных перемещается в пользовательский интерфейс, поэтому функции доступа к столбцам имеют смысл только тогда, когда необходимо выполнить некоторые дополнительные функции, такие как преобразование строки в тип продукта.
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, а затем выбирать подмножество таблицы данных для манипуляции. Действительно, вы можете пойти дальше и использовать агрегатную функцию.