在上一篇文章中,我们讨论了两种组织业务逻辑的模式:Transaction Script和Active Record。在本篇中开始讲述Domain Model和Anemic Model。
Domain Model
在开发过程中,我们常常用Domain Model来对目标的业务领域建模。通过Domain Model建模的业务类代表了目标领域中的一些概念。而且,我们会看到通过Domain Model建模的一些对象模拟了业务活动中的数据,有的对象还反映了一些业务规则。
我们就来看看电子商务系统的开发,在开发中我们建立了一些概念的模型来反映电子商务领域中的一些概念:购物车,订单,订单项等。这些模型有自己的数据,行为。例如一个订单模型,它不仅仅包含一些属性(流水号,创建日期,状态)来包含自己的数据,同时它也包含了一些业务逻辑:下订单的用户时候合法,下订单用户的余额是否充足等。
一般来说,我们对领域了解的越深,我们在软件中建立的模式越接近现实中的概念,最后实现的软件就越符合客户的需求。同时在建模的过程中,也要考虑模型的可实现行,可能我们对领域进行了很好的建模,和符合目标领域的一些概念,但是在软件实现起来非常的困难,那么就得权衡一下:找出一个比较好的模式,同时也便于实现。
在以前的文章中其实也提到过一些有关Domain Model的一些东西,其实Domain Model和Active Record的一个区别在于:Domain Model不知道自己的数据时如何持久化的,即PI(Persistence Ignorance).也就是说,通过Domain Model建立的业务类,都是POCO(Plain Old Common Runtime Object)。
下面我们就用一个银行转账的例子来讲述一下Domain Model的应用。创建一个新的解决方案,命名为ASPPatterns.Chap4.DomainModel,并且添加如下的项目:
ASPPatterns.Chap4.DomainModel.Model
ASPPatterns.Chap4.DomainModel.AppService
ASPPatterns.Chap4.DomainModel.Repository
ASPPatterns.Chap4.DomainModel.UI.Web
编译整个,Solution,然后添加引用:
为Repository项目添加Model 的引用。
为AppService项目添加Model和Repository的引用。
为Web项目添加AppService的引用。
下面就来看看每个项目代表的含义:
ASPPatterns.Chap4.DomainModel.Model:在这个project中包含了系统中所有的业务逻辑和业务对象,以及业务对象之间的关系。这个project也定义了持久化业务对象的接口,并且用Repository 模式来实现的(Repository 模式我们后面会谈到的)。大家可以看到:这个Model的project没有引用其他的project,也就是说这个Model的project完全关注于业务。
ASPPatterns.Chap4.DomainModel.Repository:这个Repository的project实现了包含在Model project中定义的持久化接口。而且Repository还引用了Model project,就是用来持久化Model的数据的。
ASPPatterns.Chap4.DomainModel.AppService:AppService project就扮演者一个应用层的角色,或者理解为门户入口,因为提供了一些比较粗颗粒度的API,并且它和Presenter层之间通过消息的机制来进行通信。(消息模式我们以后也会讲述)而且在AppService中,我们还会定义一些view model,这些view model的就符合也最后要显示的数据结构,view model的数据可能是很多业务对象数据的组合,或者仅仅就是这业务对象数据的格式转换等等。
ASPPatterns.Chap4.DomainModel.UI.Web:这个Web.UI project主要是负责最后的显示逻辑和一些用户体验的实现。这个project就调用AppService提供的API,获取符合界面显示的强类型的view model,然后显示数据。
系统的这整个结构如下:
下面就开始创建保存数据的数据库,和以前一样,为了演示的作用,我们在Web project中添加一个名为BankAccount.mdf的数据库,并且建立如下的表:
BankAccount 表
Transaction 表
下一步就开始为领域建模,因为这里的例子比较简单和常见,建模的过程就省了,最后就得到了表示领域概念的两个领域对象(或者说业务对象):
public class Transaction
{
public Transaction(decimal deposit, decimal withdrawal, string reference, DateTime date)
{
this.Deposit = deposit;
this.Withdrawal = withdrawal;
this.Reference = reference;
this.Date = date;
}
public decimal Deposit
{ get; internal set; }
public decimal Withdrawal
{ get; internal set; }
public string Reference
{ get; internal set; }
public DateTime Date
{ get; internal set; }
}
在上面的代码中,Transaction对象不包含任何的标识属性(标识对象唯一的属性,常常和数据库中的表的主键对应),因为Transaction对象就是表示订单中的每一笔交易,而且在这个系统中我们往往关心的只是每个Transaction的数据,而不关系这个Transaction到底是那个Transaction。也就是说此时在这个系统中Transaction是一个值对象(后篇讲述DDD会提到)。
再看看BankAccount类:
public class BankAccount
{
private decimal _balance;
private Guid _accountNo;
private string _customerRef;
private IList<Transaction> _transactions;
public BankAccount() : this(Guid.NewGuid(), 0, new List<Transaction>(), "")
{
_transactions.Add(new Transaction(0m, 0m, "account created", DateTime.Now));
}
public BankAccount(Guid Id, decimal balance, IList<Transaction> transactions, string customerRef)
{
AccountNo = Id;
_balance = balance;
_transactions = transactions;
_customerRef = customerRef;
}
public Guid AccountNo
{
get { return _accountNo; }
internal set { _accountNo = value; }
}
public decimal Balance
{
get { return _balance; }
internal set { _balance = value; }
}
public string CustomerRef
{
get { return _customerRef; }
set { _customerRef = value; }
}
public bool CanWithdraw(decimal amount)
{
return (Balance >= amount);
}
public void Withdraw(decimal amount, string reference)
{
if (CanWithdraw(amount))
{
Balance -= amount;
_transactions.Add(new Transaction(0m, amount, reference, DateTime.Now));
}
}
public void Deposit(decimal amount, string reference)
{
Balance += amount;
_transactions.Add(new Transaction(amount, 0m, reference, DateTime.Now));
}
public IEnumerable<Transaction> GetTransactions()
{
return _transactions;
}
}
代码中包含了一些保存数据的业务属性,同时还包含了三个简单的业务方法:
CanWithdraw:是否可以取款
Withdraw:取款
Deposit:存款
为了代码的健壮性,在调用Withdraw方法的时候,如果取款的数量超过了存款的数额,那么就抛出一个余额不足的异常:InsufficientFundsException.其实这里到底是抛异常还是给出其他的返回值,主要是个人的选择,没有一定要,非要什么的。
public class InsufficientFundsException : ApplicationException
{
}
所以业务方法Withdraw修改如下:
public void Withdraw(decimal amount, string reference)
{
if (CanWithdraw(amount))
{
Balance -= amount;
_transactions.Add(new Transaction(0m, amount, reference, DateTime.Now));
}
else
{
throw new InsufficientFundsException();
}
}
最后就考虑下如何持久化业务对象的数据。在上面业务类的设计中,我们尽量的保持业务类的干净------只包含业务逻辑,关系和业务的数据。至于数据从何而来,最后如何保存,我们都委托给了一个Repository的接口IBankAccountRepository。
public interface IBankAccountRepository
{
void Add(BankAccount bankAccount);
void Save(BankAccount bankAccount);
IEnumerable<BankAccount> FindAll();
BankAccount FindBy(Guid AccountId);
}
本系统是一个银行转账的系统,转账的操作不是一个业务对象就能够独立的完成的,往往需要多个业务类,以及数据持久化类的一些相互配合,这些操作放在任何一个业务类中都会把职责搞乱,而且后期的维护还得到处去找这个方法。所以我们在业务层中又剥离一层service,其中service中的每个方法其实和需求中的用例有个对象关系,例如在需求中就有转账的一个用例,那么在service中就有一个Transfer转账的方法,这个方法把很多的业务对象组合在一起完成这个转账的流程,也就是说,在每个业务类中的业务方法都是原子性的,细颗粒度的,可以被重用,而在业务层的service的方法就是粗颗粒度的,目的是为调用者提供简化的API。
public class BankAccountService
{
private IBankAccountRepository _bankAccountRepository;
public BankAccountService(IBankAccountRepository bankAccountRepository)
{
_bankAccountRepository = bankAccountRepository;
}
public void Transfer(Guid accountNoTo, Guid accountNoFrom, decimal amount)
{
BankAccount bankAccountTo = _bankAccountRepository.FindBy(accountNoTo);
BankAccount bankAccountFrom = _bankAccountRepository.FindBy(accountNoFrom);
if (bankAccountFrom.CanWithdraw(amount))
{
bankAccountTo.Deposit(amount, "From Acc " + bankAccountFrom.CustomerRef + " ");
bankAccountFrom.Withdraw(amount, "Transfer To Acc " + bankAccountTo.CustomerRef + " ");
_bankAccountRepository.Save(bankAccountTo);
_bankAccountRepository.Save(bankAccountFrom);
}
else
{
throw new InsufficientFundsException();
}
}
}
清楚了上面的之后,我们就把Repository那层实现,其实因为我们在业务层中使用的只是Repository的接口,至于采用哪种数据持久化方法可以替换的,例如如果用数据库来保存数据,我们可以选择用Linq To Sql,ADO.NET,EF等。业务层不用关心这些的。
在下面,就用了最原始的ADO.NET来实现的,大家可以任意替换实现策略:(下面的代码大家过过就行了,可以不用细看)
public class BankAccountRepository : IBankAccountRepository
{
private string _connectionString;
public BankAccountRepository()
{
_connectionString = ConfigurationManager.ConnectionStrings["BankAccountConnectionString"].ConnectionString;
}
public void Add(BankAccount bankAccount)
{
string insertSql = "INSERT INTO BankAccounts " +
"(BankAccountID, Balance, CustomerRef) VALUES " +
"(@BankAccountID, @Balance, @CustomerRef)";
using (SqlConnection connection =
new SqlConnection(_connectionString))
{
SqlCommand command = connection.CreateCommand();
command.CommandText = insertSql;
SetCommandParametersForInsertUpdateTo(bankAccount, command);
connection.Open();
command.ExecuteNonQuery();
}
UpdateTransactionsFor(bankAccount);
}
public void Save(BankAccount bankAccount)
{
string bankAccoutnUpdateSql = "UPDATE BankAccounts " +
"SET Balance = @Balance, CustomerRef= @CustomerRef " +
"WHERE BankAccountID = @BankAccountID;";
using (SqlConnection connection =
new SqlConnection(_connectionString))
{
SqlCommand command = connection.CreateCommand();
command.CommandText = bankAccoutnUpdateSql;
SetCommandParametersForInsertUpdateTo(bankAccount, command);
connection.Open();
command.ExecuteNonQuery();
}
UpdateTransactionsFor(bankAccount);
}
private static void SetCommandParametersForInsertUpdateTo(BankAccount bankAccount, SqlCommand command)
{
command.Parameters.Add(new SqlParameter("@BankAccountID", bankAccount.AccountNo));
command.Parameters.Add(new SqlParameter("@Balance", bankAccount.Balance));
command.Parameters.Add(new SqlParameter("@CustomerRef", bankAccount.CustomerRef));
}
private void UpdateTransactionsFor(BankAccount bankAccount)
{
string deleteTransactionSQl = "DELETE Transactions WHERE BankAccountId = @BankAccountId;";
using (SqlConnection connection =
new SqlConnection(_connectionString))
{
SqlCommand command = connection.CreateCommand();
command.CommandText = deleteTransactionSQl;
command.Parameters.Add(new SqlParameter("@BankAccountID", bankAccount.AccountNo));
connection.Open();
command.ExecuteNonQuery();
}
string insertTransactionSql = "INSERT INTO Transactions " +
"(BankAccountID, Deposit, Withdraw, Reference, [Date]) VALUES " +
"(@BankAccountID, @Deposit, @Withdraw, @Reference, @Date)";
foreach (Transaction tran in bankAccount.GetTransactions())
{
using (SqlConnection connection =
new SqlConnection(_connectionString))
{
SqlCommand command = connection.CreateCommand();
command.CommandText = insertTransactionSql;
command.Parameters.Add(new SqlParameter("@BankAccountID", bankAccount.AccountNo));
command.Parameters.Add(new SqlParameter("@Deposit", tran.Deposit));
command.Parameters.Add(new SqlParameter("@Withdraw", tran.Withdrawal));
command.Parameters.Add(new SqlParameter("@Reference", tran.Reference));
command.Parameters.Add(new SqlParameter("@Date", tran.Date));
connection.Open();
command.ExecuteNonQuery();
}
}
}
public IEnumerable<BankAccount> FindAll()
{
IList<BankAccount> accounts = new List<BankAccount>();
string queryString = "SELECT * FROM dbo.Transactions INNER JOIN " +
"dbo.BankAccounts ON dbo.Transactions.BankAccountId = dbo.BankAccounts.BankAccountId " +
"ORDER BY dbo.BankAccounts.BankAccountId;";
using (SqlConnection connection =
new SqlConnection(_connectionString))
{
SqlCommand command = connection.CreateCommand();
command.CommandText = queryString;
connection.Open();
using (SqlDataReader reader = command.ExecuteReader())
{
accounts = CreateListOfAccountsFrom(reader);
}
}
return accounts;
}
private IList<BankAccount> CreateListOfAccountsFrom(IDataReader datareader)
{
IList<BankAccount> accounts = new List<BankAccount>();
BankAccount bankAccount;
string id = "";
IList<Transaction> transactions = new List<Transaction>();
while (datareader.Read())
{
if (id != datareader["BankAccountId"].ToString())
{
id = datareader["BankAccountId"].ToString();
transactions = new List<Transaction>();
bankAccount = new BankAccount(new Guid(id), Decimal.Parse(datareader["Balance"].ToString()), transactions, datareader["CustomerRef"].ToString());
accounts.Add(bankAccount);
}
transactions.Add(CreateTransactionFrom(datareader));
}
return accounts;
}
private Transaction CreateTransactionFrom(IDataRecord rawData)
{
return new Transaction(Decimal.Parse(rawData["Deposit"].ToString()),
Decimal.Parse(rawData["Withdraw"].ToString()),
rawData["Reference"].ToString(),
DateTime.Parse(rawData["Date"].ToString()));
}
public BankAccount FindBy(Guid AccountId)
{
BankAccount account;
string queryString = "SELECT * FROM dbo.Transactions INNER JOIN " +
"dbo.BankAccounts ON dbo.Transactions.BankAccountId = dbo.BankAccounts.BankAccountId " +
"WHERE dbo.BankAccounts.BankAccountId = @BankAccountId;";
using (SqlConnection connection =
new SqlConnection(_connectionString))
{
SqlCommand command = connection.CreateCommand();
command.CommandText = queryString;
SqlParameter Idparam = new SqlParameter("@BankAccountId", AccountId);
command.Parameters.Add(Idparam);
connection.Open();
using (SqlDataReader reader = command.ExecuteReader())
{
account = CreateListOfAccountsFrom(reader)[0];
}
}
return account;
}
}
到现在为止,数据访问,业务逻辑都已经完成了,最后的一步就是显示数据了。
我们知道:最后在界面显示的数据结构,很多的时候和我们业务对象的数据结构是不一致的,这个时候我们就要进行一定的转换,生成符合界面需要的数据结构,尽量少的让显示层出来过多的逻辑。此时就引入View Model来解决问题。
AppService就是一个门户:向显示层提供需要的数据。我们在AppService中就处理数据结构不一致的情况:添加两个View Model:
public class TransactionView
{
public string Deposit { get; set; }
public string Withdrawal { get; set; }
public string Reference { get; set; }
public DateTime Date { get; set; }
}
public class BankAccountView
{
public Guid AccountNo { get; set; }
public string Balance { get; set; }
public string CustomerRef { get; set; }
public IList<TransactionView> Transactions { get; set; }
}
然后我们再添加一些辅助的方法来进行数据结构的转换,例如把Transaction转为TranactionViewModel:
public static class ViewMapper
{
public static TransactionView CreateTransactionViewFrom(Transaction tran)
{
return new TransactionView
{
Deposit = tran.Deposit.ToString("C"),
Withdrawal = tran.Withdrawal.ToString("C"),
Reference = tran.Reference,
Date = tran.Date
};
}
public static BankAccountView CreateBankAccountViewFrom(BankAccount acc)
{
return new BankAccountView
{
AccountNo = acc.AccountNo,
Balance = acc.Balance.ToString("C"),
CustomerRef = acc.CustomerRef,
Transactions = new List<TransactionView>()
};
}
}
可能现在我们是把这些project部署在一台机器上,如果是考虑到以后的分布式的情况,我们决定让显示层和AppService用消息模式来通信:请求-响应!
public abstract class ResponseBase
{
public bool Success { get; set; }
public string Message { get; set; }
}
其中Success表示方法调用是否成功,Message包含一些信息,如错误信息等。
下面就是请求的消息对象:
public class TransferRequest
{
public Guid AccountIdTo { get; set; }
public Guid AccountIdFrom { get; set; }
public decimal Amount { get; set; }
}
public class WithdrawalRequest
{
public Guid AccountId { get; set; }
public decimal Amount { get; set; }
}
然后我们把上面的对象组合在一起,为显示层提供最简化的服务:
public class ApplicationBankAccountService
{
private BankAccountService _bankAccountService;
private IBankAccountRepository _bankRepository;
public ApplicationBankAccountService() :
this (new BankAccountRepository(), new BankAccountService(new BankAccountRepository()))
{ }
public ApplicationBankAccountService(IBankAccountRepository bankRepository, BankAccountService bankAccountService)
{
_bankRepository = bankRepository;
_bankAccountService = bankAccountService;
}
public ApplicationBankAccountService(BankAccountService bankAccountService, IBankAccountRepository bankRepository)
{
_bankAccountService = bankAccountService;
_bankRepository = bankRepository;
}
public BankAccountCreateResponse CreateBankAccount(BankAccountCreateRequest bankAccountCreateRequest)
{
BankAccountCreateResponse bankAccountCreateResponse = new BankAccountCreateResponse();
BankAccount bankAccount = new BankAccount();
bankAccount.CustomerRef = bankAccountCreateRequest.CustomerName;
_bankRepository.Add(bankAccount);
bankAccountCreateResponse.BankAccountId = bankAccount.AccountNo;
bankAccountCreateResponse.Success = true;
return bankAccountCreateResponse;
}
public void Deposit(DepositRequest depositRequest)
{
BankAccount bankAccount = _bankRepository.FindBy(depositRequest.AccountId);
bankAccount.Deposit(depositRequest.Amount, "");
_bankRepository.Save(bankAccount);
}
public void Withdrawal(WithdrawalRequest withdrawalRequest)
{
BankAccount bankAccount = _bankRepository.FindBy(withdrawalRequest.AccountId);
bankAccount.Withdraw(withdrawalRequest.Amount, "");
_bankRepository.Save(bankAccount);
}
public TransferResponse Transfer(TransferRequest request)
{
TransferResponse response = new TransferResponse();
try
{
_bankAccountService.Transfer(request.AccountIdTo, request.AccountIdFrom, request.Amount);
response.Success = true;
}
catch (InsufficientFundsException)
{
response.Message = "There is not enough funds in account no: " + request.AccountIdFrom.ToString();
response.Success = false;
}
return response;
}
public FindAllBankAccountResponse GetAllBankAccounts()
{
FindAllBankAccountResponse FindAllBankAccountResponse = new FindAllBankAccountResponse();
IList<BankAccountView> bankAccountViews = new List<BankAccountView>();
FindAllBankAccountResponse.BankAccountView = bankAccountViews;
foreach (BankAccount acc in _bankRepository.FindAll())
{
bankAccountViews.Add(ViewMapper.CreateBankAccountViewFrom(acc));
}
return FindAllBankAccountResponse;
}
public FindBankAccountResponse GetBankAccountBy(Guid Id)
{
FindBankAccountResponse bankAccountResponse = new FindBankAccountResponse();
BankAccount acc = _bankRepository.FindBy(Id);
BankAccountView bankAccountView = ViewMapper.CreateBankAccountViewFrom(acc);
foreach (Transaction tran in acc.GetTransactions())
{
bankAccountView.Transactions.Add(ViewMapper.CreateTransactionViewFrom(tran));
}
bankAccountResponse.BankAccount = bankAccountView;
return bankAccountResponse;
}
}
最后我们就是处理显示层。
在本例子中,显示层就是用传统的ASP.NET来实现的,而且用了最简单的实现,如果需要,大家可以采用MVP模式,这点在我的另一文章(走向ASP.NET架构设计—第三章—分层设计,初涉架构(中篇) )中详细的讲述了,这里不在赘述,也希望大家见谅。
到这里Domain Model就基本讲述完了,我们可以看出:当软件中的业务比较的负责的时候,我们用Domain Model可能比较的好。因为用Domain Model的时候,我们的把所有的精力主要关注在对业务领域的建模,把业务的概念抽象出来,变为软件可以实现的模型。其实抽象出业务模式不是那么容易的事情,往往必须对领域作出比较深入的分析才行。
同时,在业务建模和可实现性之间要有权衡,有时候,我们把业务分析的很透,但是分析出来的概念无法转为实现,产生了“水至清则无鱼”。希望大家多多的琢磨几种组织业务逻辑模式的区别。