Domain Model
Domain Model案例,项目结构图
ASPPatterns.Chap4.DomainModel.Model:Domain Model项目将包含应用程序内所有的业务逻辑。领域对象将存放在此处,并于其他对象建立关系,从而表示应用程序正在构建的银行领域。该项目还将以接口的形式为领域对象持久化和检索定义契约,将采用Repository模式来实现所有的持久化管理需求。Model项目不会引用其他任何项目,从而确保:让它与任何基础设施关注点保持隔离,只关注业务领域。
ASPPatterns.Chap4.DomainModel.Repository:Repository项目将包含Model项目中定义的资源库接口的具体实现。Repository引用了Model项目,从而从数据库提取并持久化领域对象。Repository项目只关注领域对象持久化和检索的责任。
ASPPatterns.Chap4.DomainModel.AppService:AppService项目充当应用程序的网关(API如果愿意的话)。表示层将通过消息(简单的数据传输对象)与AppService通信。AppService层还将定义试图模型,这些是领域模型的展开试图,只用于数据显示。
ASPPatterns.Chap4.DomainModel.UI.Web:UI.Web项目负责应用程序的表示和用户体验需求。该项目只与AppService交互,并接收专门为用户体验视图创建的强类型视图模型。
在一个项目中,业务逻辑最为重要,先看存放业务逻辑的Model项目
在Model项目下创建Transaction.cs文件,Transaction对象是一个值对象。
using System; namespace ASPPatterns.Chap4.DomainModel.Model { 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; } } }
再在Model项目下创建BankAccount对象。
using System; using System.Collections.Generic; namespace ASPPatterns.Chap4.DomainModel.Model { /// <summary> /// Withdraw CanWithdraw /// </summary> public class BankAccount { decimal _balance; Guid _accountNo; string _customerRef; IList<Transaction> _transactions; public BankAccount() : this(Guid.NewGuid(), 0, new List<Transaction>(), "") { _transactions.Add(new Transaction(0m, 0m, "account Created", DateTime.Now)); } 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 BankAccount(Guid id, decimal balance, IList<Transaction> transaction, string customerRef) { _accountNo = id; _balance = balance; _transactions = transaction; _customerRef = customerRef; } /// <summary> /// 当用户尝试从某个账号取回现金的时候,先使用CanWithdraw方法来判断该用户是否可以取钱。(Test-Doer模式,先测试,再执行) /// </summary> /// <param name="amount"></param> /// <returns></returns> public bool CanWithdraw(decimal amount) { return (Balance >= amount); } /// <summary> /// 取款 /// </summary> /// <param name="amount"></param> /// <param name="reference"></param> 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();//当用户没有足够的现金金额去取款的时候,就要抛出一个异常。 } } /// <summary> /// 用户存款 /// </summary> /// <param name="amount"></param> /// <param name="reference"></param> public void Deposit(decimal amount, string reference) { Balance += amount; _transactions.Add(new Transaction(0m, amount, reference, DateTime.Now)); } public IEnumerable<Transaction> GetTransactions() { return _transactions; } } }
当用户没有足够的现金金额去取款的时候,就要抛出一个异常。所以需要创建一个类来负责抛出异常。该类仍然处于Model下。
using System; namespace ASPPatterns.Chap4.DomainModel.Model { public class InsufficientFundsException:ApplicationException { } }
现在需要某种方法来持久化BankAccount和Transactions。Model项目之定义接口,具体实现由Repository项目来实现。
using System; using System.Collections.Generic; namespace ASPPatterns.Chap4.DomainModel.Model { public interface IBankAccountRepository { void Add(BankAccount bankAccount); void Save(BankAccount bankAccount); IEnumerable<BankAccount> FindAll(); BankAccount FindBy(Guid AccountId); } }
有些动作没有很好地映射到领域实体的方法。对于这类情况,可以使用领域服务,在两个账号之间转账的动作就是一种属于服务类的责任。仍需要将BankAccountService定义在Model项目中。
using System; namespace ASPPatterns.Chap4.DomainModel.Model { public class BankAccountService { 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(); } } } }
在BankAccountService的当前实现中,在保存两个银行账号之间发生的任何错误均会让数据处于非法状态。(使用unit of work来解决这个问题)
Demo中的转账的功能大致如此,移步至Repository项目。
编写用来持久化BankAccount和Transaction业务对象的方法。BankAccountRepository需要继承先前在Model中定义的接口IBankAccountRepository,并实现接口中定义的方法。
using ASPPatterns.Chap4.DomainModel.Model; using System; using System.Collections.Generic; using System.Data.SqlClient; namespace ASPPatterns.Chap4.DomainModel.Repository { public class BankAccountRepository : IBankAccountRepository { private string _conStr; public BankAccountRepository() { _conStr = "";//可以从配置文件中获取数据库连接字符串 } public void Add(BankAccount bankAccount) { string insertSql = string.Format("Insert into BankAccounts (bankAccountId,balance,customerRef) values('{0}','{1}','{2}')", bankAccount.AccountNo, bankAccount.Balance, bankAccount.CustomerRef); using (SqlConnection connection = new SqlConnection(_conStr)) { SqlCommand command = connection.CreateCommand(); command.CommandText = insertSql; connection.Open(); command.ExecuteNonQuery(); } } public void Save(BankAccount bankAccount) { string bankAccountUpdateSql = string.Format("update bankAccounts set balance='{0}',customerRef='{1}' where bankAccountID='{2}'", bankAccount.Balance, bankAccount.CustomerRef, bankAccount.AccountNo); using (SqlConnection connection = new SqlConnection(_conStr)) { SqlCommand command = connection.CreateCommand(); command.CommandText = bankAccountUpdateSql; connection.Open(); command.ExecuteNonQuery(); } } public IEnumerable<BankAccount> FindAll() { IList<BankAccount> accounts = new List<BankAccount>(); string queryStr = "select * from transactions t inner join bankaccounts b on t.bankaccountID=b.bankAccountid order by b.bankaccountid"; using (SqlConnection connection = new SqlConnection(_conStr)) { SqlCommand command = connection.CreateCommand(); command.CommandText = queryStr; connection.Open(); using (SqlDataReader reader = command.ExecuteReader()) { //做映射,将查询到的数据赋值给accounts } } return accounts; } public BankAccount FindBy(Guid AccountId) { BankAccount account = null; string sqlCmd = string.Format("select * from transaction t inner join bankaccount b on t.bankaccountid=b.bankaccountid where b.bankaccountid='{0}'", AccountId); using (SqlConnection connection = new SqlConnection(_conStr)) { SqlCommand command = connection.CreateCommand(); command.CommandText = sqlCmd; // } return account; } } }
做好持久化方面的工作后,再看Service层
在AppService层内创建文件夹ViewModel,并在该文件夹中添加两个视图模型BankAccountView和TransactionView。这两个类的作用是用于界面显示,当BankAccount和Transaction对象中有数据,并要显示在界面上的时候,会使用映射器将BankAccount和Transaction对象中的数据映射到BankAccountView和TransactionView对象上,Demo中是手写了一个映射器,项目中可以使用AutoMapper;在ASP.NET MVC中,结合razor语言,将数据显示在界面上。
using System; using System.Collections.Generic; namespace ASPPatterns.Chap4.DomainModel.AppService.ViewModel { public class BankAccountView { public Guid AccountNo { get; set; } public string Balance { get; set; } public string CustomerRef { get; set; } public IList<TransactionView> Transactions { get; set; } } }
using System; namespace ASPPatterns.Chap4.DomainModel.AppService.ViewModel { public class TransactionView { public string Deposit { get; set; } public string Withdrawal { get; set; } public string Reference { get; set; } public DateTime Date { get; set; } } }
映射器ViewMapper
using ASPPatterns.Chap4.DomainModel.AppService.ViewModel; using ASPPatterns.Chap4.DomainModel.Model; using System.Collections.Generic; namespace ASPPatterns.Chap4.DomainModel.AppService { public static class ViewMapper { public static TransactionView CreateTransactionViewFrom(Transaction tran) { return new TransactionView { Deposit = tran.Deposit.ToString(), Withdrawal = tran.Withdrawal.ToString(), 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>() }; } } }
再在Appservice层下创建一个Messages文件夹,该文件夹中包含了所有用来与服务层通信的请求-应答对象。使用Messaging(消息传送)模式为了使让所有的API对外返回的信息格式统一。
先创建基类ResponseBase
namespace ASPPatterns.Chap4.DomainModel.AppService.Messages { public abstract class ResponseBase { public bool Success { get; set; } public string Message { get; set; } } }
创建所有的,需要实现请求和应答的对象
namespace ASPPatterns.Chap4.DomainModel.AppService.Messages { public class BankAccountCreateRequest { public string CustomerName { get; set; } } }
using System; namespace ASPPatterns.Chap4.DomainModel.AppService.Messages { public class BankAccountCreateReponse : ResponseBase { public Guid BankAccountId { get; set; } } }
using System; namespace ASPPatterns.Chap4.DomainModel.AppService.Messages { public class DepositRequest { public Guid AccountId { get; set; } public decimal Amount { get; set; } } }
using ASPPatterns.Chap4.DomainModel.AppService.ViewModel; using System.Collections.Generic; namespace ASPPatterns.Chap4.DomainModel.AppService.Messages { public class FindAllBankAccountResponse : ResponseBase { public IList<BankAccountView> BankAccounView { get; set; } } }
using ASPPatterns.Chap4.DomainModel.AppService.ViewModel; namespace ASPPatterns.Chap4.DomainModel.AppService.Messages { public class FindBankAccountResponse { public BankAccountView BankAccount { get; set; } } }
using System; namespace ASPPatterns.Chap4.DomainModel.AppService.Messages { public class TransferRequest { public Guid AccountIdTo { get; set; } public Guid AccountIdFrom { get; set; } public decimal Amount { get; set; } } }
namespace ASPPatterns.Chap4.DomainModel.AppService.Messages { public class TransferResponse : ResponseBase { } }
using System; namespace ASPPatterns.Chap4.DomainModel.AppService.Messages { public class WithdrawalRequest { public Guid AccountId { get; set; } public decimal Amount { get; set; } } }
ApplicationBankAccountService类协调应用程序活动并将所有的业务任务委托给领域模型。该层并不包含任何业务逻辑,有助于防止任何与业务无关的代码污染领域模型项目。
该层还将领域实体转换成数据传输对象,从而保护领域的内部操作,并为一起工作的表示层提供了一个易于使用的API。
using ASPPatterns.Chap4.DomainModel.AppService.Messages; using ASPPatterns.Chap4.DomainModel.AppService.ViewModel; using ASPPatterns.Chap4.DomainModel.Model; using ASPPatterns.Chap4.DomainModel.Repository; using System; using System.Collections.Generic; namespace ASPPatterns.Chap4.DomainModel.AppService { /// <summary> /// 该类协调应用程序活动,并将所有的业务任务委托给领域模型。该层并不包含任何业务逻辑。有助于防止任何与业务无关的代码污染领域模型项目。 /// 该层还将领域实体转换成数据传输对象,从而保护领域内部操作,并为一起工作的表示层提供了一个易于使用的API /// </summary> public class ApplicationBankAccountService { BankAccountService _bankAccountService; IBankAccountRepository _bankRepository; public ApplicationBankAccountService() : this(new BankAccountRepository(), new BankAccountService(new BankAccountRepository())) { } public ApplicationBankAccountService(IBankAccountRepository bankRepository, BankAccountService bankAccountService) { _bankRepository = bankRepository; _bankAccountService = bankAccountService; } public BankAccountCreateReponse CreateBankAccount(BankAccountCreateRequest bankAccountCreateRequest) { BankAccountCreateReponse bankAccountCreateReponse = new BankAccountCreateReponse(); BankAccount bankAccount = new BankAccount(new Guid(), 12.12M, new List<Transaction>(), "customerRef"); bankAccount.CustomerRef = bankAccountCreateRequest.CustomerName; _bankRepository.Add(bankAccount); return bankAccountCreateReponse; } 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 (Exception) { 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.BankAccounView = 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; } } }
在UI层调用Service。UI曾的界面没有展示,那不是重点。重点考察如何在UI层中调用Service。
using ASPPatterns.Chap4.DomainModel.AppService; using ASPPatterns.Chap4.DomainModel.AppService.Messages; using ASPPatterns.Chap4.DomainModel.AppService.ViewModel; using System; using System.Web.UI; namespace ASPPatterns.Chap4.DomainModel.UI.Web { public partial class Default : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { if (!Page.IsPostBack) { ShowAllAccounts(); } } private void ShowAllAccounts() { ddlBankAccounts.Items.Clear(); FindAllBankAccountResponse response = new ApplicationBankAccountService().GetAllBankAccounts(); ddlBankAccounts.Items.Add(new ListItem("Select An Account", "")); foreach (BankAccountView accView in response.BankAccounView) { ddlBankAccounts.Items.Add(new ListItem(accView.CustomerRef, accView.AccountNo.ToString())); } } protected void btCreateAccount_Click(object sender, EventArgs e) { BankAccountCreateRequest createAccountRequest = new BankAccountCreateRequest(); ApplicationBankAccountService service = new ApplicationBankAccountService(); service.CreateBankAccount(createAccountRequest); ShowAllAccounts(); } protected void ddlBankAccounts_SelectedIndexChanged(object sender, EventArgs e) { DisplaySelectedAccount(); } private void DisplaySelectedAccount() { if (ddlBankAccounts.SelectedValue.ToString() != "") { ApplicationBankAccountService service = new ApplicationBankAccountService(); FindBankAccountResponse response = service.GetBankAccountBy(new Guid()); BankAccountView accView = response.BankAccount; lblAccount.Text = accView.Balance.ToString(); lblBalance.Text = accView.Balance.ToString(); lblCustomerRef.Text = accView.CustomerRef; rptTransactions.DataSource = accView.Transactions; rptTransactions.DataBind(); FindAllBankAccountResponse allAccountResponse = service.GetAllBankAccounts(); ddlBankAccountsToTransferTo.Items.Clear(); foreach (var acc in allAccountResponse.BankAccounView) { if (acc.AccountNo.ToString() != ddlBankAccounts.SelectedValue.ToString()) ddlBankAccountsToTransferTo.Items.Add(new ListItem(acc.CustomerRef, acc.AccountNo.ToString())); } } } protected void btnWithdrawal_Click(object sender, EventArgs e) { ApplicationBankAccountService service = new ApplicationBankAccountService(); WithdrawalRequest request = new WithdrawalRequest(); Guid guid = new Guid(); request.AccountId = guid; request.Amount = decimal.Parse(txtAmount.Text); service.Withdrawal(request); DisplaySelectedAccount(); } protected void btnDeposit_Click(object sender, EventArgs e) { ApplicationBankAccountService service = new ApplicationBankAccountService(); DepositRequest request = new DepositRequest(); Guid guid = new Guid(); request.AccountId = guid; request.Amount = decimal.Parse(txtAmount.Text); service.Deposit(request); DisplaySelectedAccount(); } } }