本篇同步發布於個人Blog: [PoEAA] Domain Logic Pattern - Domain Model
According to [PoEAA], this definition is "A Domain Model creates a web of interconnected objects, where each object represents some meaningful individual, whether as large as a corporation or as small as a single line on an order form."
Domain Model has two kinds of type:
Simple Domain Model: Every domain model can be mapped to one database table. Usually it uses Active Record.
Complex Domain Model: The model uses inheritance, strategies and other design patterns. Database can not directly map to the model. Using Data Mapper is required.
If the business layer is frequently changed and is gradually complex, using Domain Model is a good choice. Creating Service Layer wraps the Domain Model to provide unified API for outer modules.
This problem is introduced in the previous article [PoEAA] Domain Logic Pattern - Transaction Script. This article uses Domain Model to build the domain layer.
Define the classes for the revenue recognition, product and contract as the following figure:
Figure 1: Class diagram of Revenue Recognition example
This pattern is implemented by C#. Because the section Chapter 9 Domain Logic Pattern - Domain Model of PoEAA didn't implement with database integration, this implementation don't contain database example.
For Contract class, it contains a Money object _revenue and a DateTime object _whenSigned. It also contains a list of RevenueRecognition which these recognitions belong to this contract.
class Contract
{
private readonly List<RevenueRecognition> _revenueRecognitions = new List<RevenueRecognition>();
private readonly Product _product;
private readonly Money _revenue;
private readonly DateTime _whenSigned;
private readonly int _id;
private static int _commonId = 1;
private static readonly object IdLock = new object();
public Contract(Product product, Money revenue, DateTime whenSigned)
{
_product = product;
_revenue = revenue;
_whenSigned = whenSigned;
_id = GenerateNewId();
}
private static int GenerateNewId()
{
// todo db generate auto increment id
lock (IdLock)
{
return _commonId++;
}
}
public Money RecognizedRevenue(DateTime asOf)
{
Money result = Money.Dollars(0m);
_revenueRecognitions.ForEach(x =>
{
if (x.IsRecognizableBy(asOf))
{
result += x.GetAmount();
}
});
return result;
}
public Money GetRevenue()
{
return _revenue;
}
public DateTime GetWhenSigned()
{
return _whenSigned;
}
public void AddRevenueRecognition(RevenueRecognition revenueRecognition)
{
// todo
// db insert
// after db insertion, add it to list
_revenueRecognitions.Add(revenueRecognition);
}
public void CalculateRecognitions()
{
_product.CalculateRevenueRecognitions(this);
}
}
class RevenueRecognition
{
private readonly Money _amount;
private readonly DateTime _date;
public RevenueRecognition(Money amount, DateTime date)
{
_amount = amount;
_date = date;
}
public Money GetAmount()
{
return _amount;
}
public bool IsRecognizableBy(DateTime asOf)
{
return asOf.CompareTo(_date) >= 0;
}
}
For Product class, it contains a name and associates a RecognitionStrategy instance:
class Product
{
private readonly string _name;
private readonly RecognitionStrategy _recognitionStrategy;
public Product(string name, RecognitionStrategy recognitionStrategy)
{
_name = name;
_recognitionStrategy = recognitionStrategy;
}
public static Product NewWordProcessor(string name)
{
return new Product(name, new CompleteRecognitionStrategy());
}
public static Product NewSpreadsheet(string name)
{
return new Product(name, new ThreeWayRecognitionStrategy(60, 90));
}
public static Product NewDatabase(string name)
{
return new Product(name, new ThreeWayRecognitionStrategy(30, 60));
}
public void CalculateRevenueRecognitions(Contract contract)
{
_recognitionStrategy.CalculateRevenueRecognitions(contract);
}
}
Previously a Transaction Script calculated the revenue recognitions in one service function. This Domain Model uses Strategy Pattern to bind Product instance with a RecognitionStrategy instance. The detail of calculating revenue recognition is implemented by the associated strategy.
RecognitionStrategy is a base class that declare a abstract CalculateRevenueRecognitions function.
CompleteRecognitionStrategy inherits from RecognitionStrategy and allocates all revenue in one date as same as Contract's signed date.
ThreeWayRecognitionStrategy also inherits from RecognitionStrategy and allocates revenue into 3 date.
abstract class RecognitionStrategy
{
public abstract void CalculateRevenueRecognitions(Contract contract);
}
class CompleteRecognitionStrategy : RecognitionStrategy
{
public override void CalculateRevenueRecognitions(Contract contract)
{
contract.AddRevenueRecognition(new RevenueRecognition(contract.GetRevenue(), contract.GetWhenSigned()));
}
}
class ThreeWayRecognitionStrategy : RecognitionStrategy
{
private readonly int _firstRecognitionOffset;
private readonly int _secondRecognitionOffset;
public ThreeWayRecognitionStrategy(int firstRecognitionOffset, int secondRecognitionOffset)
{
_firstRecognitionOffset = firstRecognitionOffset;
_secondRecognitionOffset = secondRecognitionOffset;
}
public override void CalculateRevenueRecognitions(Contract contract)
{
Money[] allocation = contract.GetRevenue().Allocate(3);
contract.AddRevenueRecognition(new RevenueRecognition(allocation[0], contract.GetWhenSigned()));
contract.AddRevenueRecognition(new RevenueRecognition(allocation[1], contract.GetWhenSigned().AddDays(_firstRecognitionOffset)));
contract.AddRevenueRecognition(new RevenueRecognition(allocation[2], contract.GetWhenSigned().AddDays(_secondRecognitionOffset)));
}
}
Create a console program and create 3 Products and 3 Contracts to calculate the revenue recognitions for the 3 products.
As the following code:
Product word = Product.NewWordProcessor("CodeParadise Word");
Product calc = Product.NewSpreadsheet("CodeParadise Calc");
Product db = Product.NewDatabase("CodeParadise DB");
Contract wordContract = new Contract(word, Money.Dollars(24000m), new DateTime(2020, 7, 25));
Contract calcContract = new Contract(calc, Money.Dollars(1000m), new DateTime(2020, 3, 15));
Contract dbContract = new Contract(db, Money.Dollars(9999m), new DateTime(2020, 1, 1));
wordContract.CalculateRecognitions();
calcContract.CalculateRecognitions();
dbContract.CalculateRecognitions();
var wordProcessorRevenue = wordContract.RecognizedRevenue(new DateTime(2020, 9, 30));
Console.WriteLine($"word processor revenue before 2020-09-30 = {wordProcessorRevenue.Amount}");
var spreadsheetRevenue = calcContract.RecognizedRevenue(new DateTime(2020, 6, 1));
Console.WriteLine($"spreadsheet revenue before 2020-06-01 = {spreadsheetRevenue.Amount}");
var databaseRevenue = dbContract.RecognizedRevenue(new DateTime(2020, 1, 25));
Console.WriteLine($"database revenue before 2020-01-25 = {databaseRevenue.Amount}");
The console shows:
This result is the same as the Transaction Script example.
"Domain Model" is the necessary pattern for domain logic. It can be maintainable and testable for modern complex applications. If you are a .NET developer, Entity Framework or other popular ORMs have the concept of this pattern.
Usually a complex application not only uses Domain Model to satisfy requirements, but also integrates other layers including presentation/service/persistence... Every layer is loosely coupled so the testing/refactoring will be more liable than Transaction Script.
For next article I will write Table Module pattern according to Chapter 9 Domain Logic Pattern - Table Module of PoEAA.
Patterns of Enterprise Application Architecture Book(Amazon)