在上一篇文章中,我们讨论了两种组织业务逻辑的模式: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(