走向ASP.NET架構設計——第四章—業務層分層架構(中篇)
在上一篇文章中,我們討論了兩種組織業務邏輯的模式: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 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類:
{
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.其實這里到底是拋異常還是給出其他的返回值,主要是個人的選擇,沒有一定要,非要什么的。
{
}
所以業務方法Withdraw修改如下:
{
if (CanWithdraw(amount))
{
Balance -= amount;
_transactions.Add(new Transaction(0m, amount, reference, DateTime.Now));
}
else
{
throw new InsufficientFundsException();
}
}
最后就考慮下如何持久化業務對象的數據。在上面業務類的設計中,我們盡量的保持業務類的干凈------只包含業務邏輯,關系和業務的數據。至于數據從何而來,最后如何保存,我們都委托給了一個Repository的接口IBankAccountRepository。
{
void Add(BankAccount bankAccount);
void Save(BankAccount bankAccount);
IEnumerable<BankAccount> FindAll();
BankAccount FindBy(Guid AccountId);
}
本系統是一個銀行轉賬的系統,轉賬的操作不是一個業務對象就能夠獨立的完成的,往往需要多個業務類,以及數據持久化類的一些相互配合,這些操作放在任何一個業務類中都會把職責搞亂,而且后期的維護還得到處去找這個方法。所以我們在業務層中又剝離一層service,其中service中的每個方法其實和需求中的用例有個對象關系,例如在需求中就有轉賬的一個用例,那么在service中就有一個Transfer轉賬的方法,這個方法把很多的業務對象組合在一起完成這個轉賬的流程,也就是說,在每個業務類中的業務方法都是原子性的,細顆粒度的,可以被重用,而在業務層的service的方法就是粗顆粒度的,目的是為調用者提供簡化的API。
{
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來實現的,大家可以任意替換實現策略:(下面的代碼大家過過就行了,可以不用細看)
{
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 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 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 bool Success { get; set; }
public string Message { get; set; }
}
其中Success表示方法調用是否成功,Message包含一些信息,如錯誤信息等。
下面就是請求的消息對象:
{
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; }
}
然后我們把上面的對象組合在一起,為顯示層提供最簡化的服務:
{
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的時候,我們的把所有的精力主要關注在對業務領域的建模,把業務的概念抽象出來,變為軟件可以實現的模型。其實抽象出業務模式不是那么容易的事情,往往必須對領域作出比較深入的分析才行。
同時,在業務建模和可實現性之間要有權衡,有時候,我們把業務分析的很透,但是分析出來的概念無法轉為實現,產生了“水至清則無魚”。希望大家多多的琢磨幾種組織業務邏輯模式的區別。