应用各种领域逻辑模式组织业务逻辑层
并不是所有的应用程序都需要复杂的体系结构来封装业务逻辑,只有深入理解所有的领域逻辑模式的优缺点及使用场景,才能应用最合适的模式去解决你所面临的问题。目前最流行的领域逻辑模式有:Transaction Script(事务脚本)、Active Record(活动记录)、Domain Model(领域模型)和Anemic Domain Model(贫血领域模型)。下面我将分别讲述这几种模式的优缺点及使用场景,希望大家可以在实际项目中加以利用发挥一定作用。
1. Transaction Script(事务脚本):所谓事务脚本,是一种面向过程而非面向对象的业务逻辑方法。在该过程中包含相对独立的业务事务,为每个业务创建一个独立的方法(如工作流、数据库持久化验证检查等),将这些业务事务组合起来形成相应的业务逻辑,进而完成相应的功能。这种模式的优势在于出现新需求时,可以向类中添加更多的方法而不会影响到现有的功能,适合于含有少量业务逻辑或不太可能增加的功能集合的小型应用程序。下面以一个人力资源休假的应用程序来说明:
public class HolidayService
{
public static bool BookHolidayFor(int employeeId, DateTime From, DateTime To)
{
bool booked = false;
TimeSpan numberOfDaysRequestedForHoliday = To - From;
if (numberOfDaysRequestedForHoliday.Days > 0)
{
if (RequestHolidayDoesNotClashWithExistingHoliday(employeeId, From, To))
{
int holidayAvailable = GetHolidayRemainingFor(employeeId);
if (holidayAvailable >= numberOfDaysRequestedForHoliday.Days)
{
SubmitHolidayBookingFor(employeeId, From, To);
booked = true;
}
}
}
return booked;
}
private static int GetHolidayRemainingFor(int employeeId)
{
// ...
}
public static List<EmployeeDTO> GetAllEmployeesOnLeaveBetween(DateTime From, DateTime To)
{
// ...
}
public static List<EmployeeDTO> GetAllEmployeesWithHolidayRemaining()
{
// ...
}
}
可以看出,整个业务实例被封装到一个单独的方法中。BookHolidayFor方法处理许多责任,如数据检索和持久化,以及用来确定是否可以休假的业务逻辑。这种过程式编程风格违背了面向对象编程的基本理念。如果应用程序较小,而且业务逻辑简单,不需要采用完全的面向对象方法,Transaction Script模式可能比较合适。但是,如果应用程序规模会变大,那就可能需要重新考虑业务逻辑结构并寻求更具伸缩性的模式,如Active Record模式。
2. Active Record(活动记录): 所谓活动记录,是一种底层数据库模型和业务对象模型相匹配的一种流行模式。通常,数据库中的每张表都对应一个业务对象。业务对象表示表中的一行,并且包含数据、行为以及持久化该对象的工具,此外还有添加新实例和查找对象集合所需的方法。在Active Record模式中,每个业务对象均负责自己的持久化和相关的业务逻辑。非常适用于在数据模型和业务模型之间具有一对一映射关系的简单应用程序,与Transaction Script模式一样,Active Record模式也非常简单而且易于掌握。下面以一个博客网站为例,来剖析该模式的具体应用。首先看一下数据结构图:
然后导航到www.castleproject.org/castle/download.html并下载ActiveRecord项目的最新版本,下面开始搭建项目ActiveRecord.Model,ActiveRecord.Mvc,并引入ActiveReord框架(Castle.ActiveRecord.dll、NHibernate.dll),以下我的项目示例的文件结构和主要代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Castle.ActiveRecord;
using Castle.ActiveRecord.Queries;
namespace ActiveRecord.Model
{
[ActiveRecord("Posts")]
public class Post : ActiveRecordBase<Post>
{
[PrimaryKey]
public int ID { get; set; }
[Property]
public string Subject { get; set; }
[Property]
public string Text { get; set; }
[Property]
public DateTime CreateTime { get; set; }
[HasMany]
public IList<Comment> Comments { get; set; }
public string ShortText
{
get
{
return Text.Length > 20 ? Text.Substring(0, 20) + "..." : Text;
}
}
public static Post FindLatestPost()
{
SimpleQuery<Post> posts = new SimpleQuery<Post>("from Post p order by p.CreateTime desc");
return posts.Execute()[0];
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Castle.ActiveRecord;
namespace ActiveRecord.Model
{
[ActiveRecord("Comments")]
public class Comment : ActiveRecordBase<Comment>
{
[PrimaryKey]
public int ID { get; set; }
[Property]
public string Text { get; set; }
[Property]
public string Author { get; set; }
[Property]
public DateTime CreateTime { get; set; }
[BelongsTo("PostID")]
public Post Post { get; set; }
}
}
BlogMaster.Master:
<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<link href="http://www.cnblogs.com/Content/Site.css" rel="stylesheet" type="text/css" />
<title><asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title>
</head>
<body>
<div id="document">
<div id="header"><h1>My Blog</h1></div>
<div id="nav"><%= Html.ActionLink("Create Post","Create") %></div>
<asp:ContentPlaceHolder ID="MainContent" runat="server">
</asp:ContentPlaceHolder>
</div>
</body>
</html>
Index.aspx:
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/BlogMaster.Master"
Inherits="System.Web.Mvc.ViewPage" %>
<%@ Import Namespace="ActiveRecord.Model" %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<div id="content">
<h2>
<%=Html.Encode(((Post)ViewData["LatestPost"]).Subject)%></h2>
<%=((Post)ViewData["LatestPost"]).Text.Replace("<\n>","<br/>")%><br />
<i>posted on <%=Html.Encode(((Post)ViewData["LatestPost"]).CreateTime.ToLongDateString())%></i>
<hr />
Comments<br />
<%foreach (var item in ((Post)ViewData["LatestPost"]).Comments)
{
%>
<p>
<i>
<%=Html.Encode(item.Author) %> said on
<%=Html.Encode(item.CreateTime.ToLongDateString())%>
at
<%=Html.Encode(item.CreateTime.ToLongTimeString())%>...</i><br />
<%=Html.Encode(item.Text) %>
</p>
<%
}%>
<p>
Add a comment</p>
<%using (Html.BeginForm("CreateComment", "Blog", new { ID = ((Post)ViewData["LatestPost"]).ID }, FormMethod.Post))
{ %>
<p>
Your name<br />
<%=Html.TextBox("Author") %></p>
<p>
Your comment<br />
<%=Html.TextArea("Comment") %></p>
<p>
<input type="submit" value="Add Comment" /></p>
<%} %>
</div>
<div id="rightNav">
<h2>
All Posts</h2>
<ul>
<%foreach (var item in (Post[])ViewData["AllPosts"])
{ %>
<li>
<%=Html.ActionLink(item.Subject, "Detail", new { ID = item.ID})%><br />
<%=Html.Encode(item.ShortText) %>
</li>
<%} %></ul>
</div>
</asp:Content>
AddPost.aspx:
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/BlogMaster.Master"
Inherits="System.Web.Mvc.ViewPage" %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<%using (Html.BeginForm("Create","Blog"))
{ %>
<p>
Subject<br />
<%=Html.TextBox("Subject") %></p>
<p>
Content<br />
<%=Html.TextArea("Content")%>
</p>
<p>
<input type="submit" value="Create" />
</p>
<%} %>
</asp:Content>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using ActiveRecord.Model;
namespace ActiveRecord.Mvc.Controllers
{
public class BlogController : Controller
{
// GET: /Blog/
public ActionResult Index()
{
Post[] posts = Post.FindAll();
if (posts.Count() > 0)
{
ViewData["AllPosts"] = posts;
ViewData["LatestPost"] = Post.FindLatestPost();
return View();
}
else
{
return Create();
}
}
// POST: /Blog/
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult CreateComment(string id, FormCollection collection)
{
int postId = 0;
int.TryParse(id, out postId);
Post post = Post.Find(postId);
Comment comment = new Comment()
{
Post = post,
Author = Request.Form["Author"],
Text = Request.Form["Comment"],
CreateTime = DateTime.Now
};
comment.Save();
return Detail(post.ID.ToString());
}
// GET: /Blog/Detail/1
public ActionResult Detail(string id)
{
ViewData["AllPosts"] = Post.FindAll();
int postId = 0;
int.TryParse(id, out postId);
ViewData["LatestPost"] = Post.Find(postId);
return View("Index");
}
// GET: /Blog/Create
public ActionResult Create()
{
return View("AddPost");
}
// POST: /Blog/Create
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(FormCollection collection)
{
Post post = new Post()
{
Subject = Request.Form["Subject"],
Text = Request.Form["Content"],
CreateTime = DateTime.Now
};
post.Save();
return Detail(post.ID.ToString());
}
}
}
#document
{
width: 750px;
margin: 0 auto;
}
#content
{
float: left;
width: 500px;
}
#rightNav
{
float: right;
width: 250px;
}
using ActiveRecord.Model;
using Castle.ActiveRecord.Framework;
public class MvcApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // 路由名称
"{controller}/{action}/{id}", // 带有参数的 URL
new { controller = "Blog", action = "Index", id = "" } // 参数默认值
);
}
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
IConfigurationSource source = ConfigurationManager.GetSection("activeRecord") as IConfigurationSource;
Castle.ActiveRecord.ActiveRecordStarter.Initialize(source, typeof(Post), typeof(Comment));
}
}
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="activeRecord"
type="Castle.ActiveRecord.Framework.Config.ActiveRecordSectionHandler, Castle.ActiveRecord" />
</configSections>
<activeRecord isWeb="true">
<config>
<add key="hibernate.connection.driver_class" value="NHibernate.Driver.SqlClientDriver"/>
<add key="dialect" value="NHibernate.Dialect.MsSql2005Dialect"/>
<add key="hibernate.connection.provider" value="NHibernate.Connection.DriverConnectionProvider"/>
<add key="connection.connection_string" value="server=*****;uid=sa;pwd=*****;database=Northwind;"/>
<add key="proxyfactory.factory_class" value="NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle"/>
</config>
</activeRecord>
</configuration>
这里以极快的速度构建好博客应用程序,这在很大程度上要归功于Castle ActiveRecord框架,由于对象模型与数据模型之间紧密相关,它能够将数据的检索和访问操作自动化。Active Record模式并非灵丹妙药。它擅长于处理底层数据模型能够很好映射到业务模型的情形,但是当出现不匹配时(有时候称为阻抗失配),该模式将很难应对。这是由于复杂系统的概念业务模型有时与数据模型不同所造成的。如果业务领域非常丰富,有着大量的复杂规则、逻辑和工作流,那么采用Domain Model方法将更加有利。
3. Domain Model(领域模型): 所谓领域模型,是一种用来处理复杂的业务逻辑、规则和验证过程的事物之间关系的模式。Domain Model与Active Record模式之间的主要差别在于,Domain Model中存在的业务实体并不知道如何持久化自己,而且没有必要在数据模型和业务模型之间建立一对一的映射关系。我准备创建一个解决方案来为银行领域建模,这涉及账号的创建以及账号之间的现金转账,以下是我的项目示例和文件结构和主要代码:
在这里,我说明一下各层的主要功能:
DomainModel.Model 将包含应用程序内的所有业务逻辑。领域对象将存放在此处,并与其他对象建立关系,从而表示应用程序正在构建的银行领域。该项目还将以接口的形式为领域对象持久化和检索定义契约,将采用Repository模式来实现所有的持久化管理需求。
public class Account
{
private IList<Transaction> transactions;
public Guid AccountID { get; internal set; }
public decimal Balance { get; internal set; }
public string Customer { get; set; }
public IEnumerable<Transaction> Transactions
{
get { return this.transactions; }
}
public Account()
: this(Guid.NewGuid(), 0m, "", new List<Transaction>())
{
this.transactions.Add(new Transaction(0m, 0m, "Create a new account", DateTime.Now));
}
public Account(Guid id, decimal balance, string customer, IList<Transaction> trans)
{
this.AccountID = id;
this.Balance = balance;
this.Customer = customer;
this.transactions = trans;
}
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));
}
else
{
throw new InsufficientFundsException();
}
}
public void Deposit(decimal amount, string reference)
{
Balance += amount;
transactions.Add(new Transaction(amount, 0m, reference, DateTime.Now));
}
}
public class Transaction
{
public decimal Deposit { get; internal set; }
public decimal Withdraw { get; internal set; }
public string Reference { get; internal set; }
public DateTime TransDate { get; internal set; }
public Transaction(decimal deposit, decimal withdraw, string reference, DateTime transdate)
{
this.Deposit = deposit;
this.Withdraw = withdraw;
this.Reference = reference;
this.TransDate = transdate;
}
}
public class InsufficientFundsException : ApplicationException
{
//...
}
public class AccountNotBeFoundException : ApplicationException
{
//...
}
/// <summary>
/// 定义领域实体映射方法
/// </summary>
public interface IAccountRepository
{
void Add(Account account);
void Save(Account account);
IEnumerable<Account> FindAll();
Account FindBy(Guid id);
}
public class AccountService
{
private IAccountRepository accountRepository;
public AccountService(IAccountRepository accountRepository)
{
this.accountRepository = accountRepository;
}
public void Transfer(Guid from, Guid to, decimal amount)
{
var accountFrom = accountRepository.FindBy(from);
var accountTo = accountRepository.FindBy(to);
if (accountFrom == null || accountTo == null)
{
throw new AccountNotBeFoundException();
}
if (!accountFrom.CanWithdraw(amount))
{
throw new InsufficientFundsException();
}
accountFrom.Withdraw(amount, "Transfer from " + accountFrom.Customer + " 's fund: $" + amount);
accountTo.Deposit(amount, "Deposit to " + accountTo.Customer + " 's fund: $" + amount);
accountRepository.Save(accountFrom);
accountRepository.Save(accountTo);
}
}
DomainModel.Repository 将包含Model项目中定义的资源库接口的具体实现。Repository引用了Model项目,从而从数据库提取并持久化领域对象。Repository项目只关注领域对象持久化和检索的责任。
public class AccountRepository : IAccountRepository
{
private readonly string ConnectionString = ConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
public void Add(Account account)
{
var strSql = new StringBuilder();
strSql.Append(@"INSERT INTO Accounts(AccountID, Balance, Customer)
VALUES(@AccountID, @Balance, @Customer)");
AddOrSaveAccount(strSql.ToString(), account);
}
public void Save(Account account)
{
var strSql = new StringBuilder();
strSql.Append(@"UPDATE Accounts
SET Balance = @Balance, Customer = @Customer
WHERE AccountID = @AccountID");
AddOrSaveAccount(strSql.ToString(), account);
}
public IEnumerable<Account> FindAll()
{
IList<Account> accounts = new List<Account>();
var strSql = new StringBuilder();
strSql.Append(@"SELECT * FROM Accounts a LEFT JOIN
Transactions t ON a.AccountID = t.AccountID
ORDER BY a.AccountID");
var ds = SqlHelper.ExecuteDataset(ConnectionString, CommandType.Text, strSql.ToString());
if (ds != null && ds.Tables.Count > 0)
{
foreach (DataRow dr in ds.Tables[0].Rows)
{
var id = dr["AccountID"].ConvertToString();
if (String.IsNullOrEmpty(id)) continue;
var trans = new List<Transaction>();
trans.Add(new Transaction(
dr["Deposit"].ConvertToDecimal(),
dr["Withdraw"].ConvertToDecimal(),
dr["Reference"].ConvertToString(),
dr["Transdate"].ConvertToDateTime()));
accounts.Add(new Account(new Guid(id),
dr["Balance"].ConvertToDecimal(),
dr["Customer"].ConvertToString(),
trans));
}
}
return accounts;
}
public Account FindBy(Guid id)
{
var account = new Account();
var strSql = new StringBuilder();
strSql.Append(@"SELECT * FROM Accounts a LEFT JOIN
Transactions t ON a.AccountID = t.AccountID
WHERE a.AccountID = @AccountID");
var paramList = new List<SqlParameter>();
paramList.Add(new SqlParameter("@AccountID", id));
var ds = SqlHelper.ExecuteDataset(ConnectionString, CommandType.Text, strSql.ToString(), paramList.ToArray());
if (ds != null && ds.Tables.Count > 0)
{
foreach (DataRow dr in ds.Tables[0].Rows)
{
var acctId = dr["AccountID"].ConvertToString();
if (String.IsNullOrEmpty(acctId)) continue;
var trans = new List<Transaction>();
trans.Add(new Transaction(
dr["Deposit"].ConvertToDecimal(),
dr["Withdraw"].ConvertToDecimal(),
dr["Reference"].ConvertToString(),
dr["Transdate"].ConvertToDateTime()));
account = new Account(new Guid(acctId),
dr["Balance"].ConvertToDecimal(),
dr["Customer"].ConvertToString(),
trans);
}
}
return account;
}
private void AddOrSaveAccount(string sql, Account account)
{
var paramList = new List<SqlParameter>();
paramList.Add(new SqlParameter("@AccountID", account.AccountID));
paramList.Add(new SqlParameter("@Balance", account.Balance));
paramList.Add(new SqlParameter("@Customer", account.Customer));
var rows = SqlHelper.ExecuteNonQuery(ConnectionString, CommandType.Text, sql, paramList.ToArray());
if (rows > 0)
{
UpdateTransactionsFor(account);
}
}
private void UpdateTransactionsFor(Account account)
{
var strSql = new StringBuilder();
strSql.Append(@"DELETE Transactions WHERE AccountID = @AccountID");
var paramList = new List<SqlParameter>();
paramList.Add(new SqlParameter("@AccountID", account.AccountID));
SqlHelper.ExecuteNonQuery(ConnectionString, CommandType.Text, strSql.ToString(), paramList.ToArray());
strSql = new StringBuilder();
foreach (Transaction trans in account.Transactions)
{
strSql.Append(@"INSERT INTO Transactions
(AccountID, Deposit, Withdraw, Reference, TransDate)
VALUES(@AccountID, @Deposit, @Withdraw, @Reference, @TransDate)");
paramList = new List<SqlParameter>();
paramList.Add(new SqlParameter("@AccountID", account.AccountID));
paramList.Add(new SqlParameter("@Deposit", trans.Deposit));
paramList.Add(new SqlParameter("@Withdraw", trans.Withdraw));
paramList.Add(new SqlParameter("@Reference", trans.Reference));
paramList.Add(new SqlParameter("@TransDate", trans.TransDate));
SqlHelper.ExecuteNonQuery(ConnectionString, CommandType.Text, strSql.ToString(), paramList.ToArray());
}
}
}
DomainModel.Service 将充当应用程序的网关(API,如果愿意的话)。表示层将通过消息(简单的数据传输对象)与AppService通信。还将定义视图模型,这些是领域模型的展开视图,只用于数据显示。
public class AccountView
{
public Guid AccountID { get; set; }
public string Balance { get; set; }
public string Customer { get; set; }
public IList<TransactionView> Transactions { get; set; }
}
public class TransactionView
{
public string Deposit { get; set; }
public string Withdraw { get; set; }
public string Reference { get; set; }
public DateTime TransDate { get; set; }
}
public static class ViewMapper
{
public static AccountView CreateAccountView(Account account)
{
return new AccountView()
{
AccountID = account.AccountID,
Balance = account.Balance.ToString("C"),
Customer = account.Customer,
Transactions = new List<TransactionView>()
};
}
public static TransactionView CreateTransactionView(Transaction trans)
{
return new TransactionView()
{
Deposit = trans.Deposit.ToString("C"),
Withdraw = trans.Withdraw.ToString("C"),
Reference = trans.Reference,
TransDate = trans.TransDate
};
}
}
public class ApplicationAccountService
{
private AccountService service;
private IAccountRepository repository;
public ApplicationAccountService()
: this(new AccountService(new AccountRepository()), new AccountRepository())
{
}
public ApplicationAccountService(AccountService service, IAccountRepository repository)
{
this.service = service;
this.repository = repository;
}
public AccountCreateResponse CreateAccount(AccountCreateRequest request)
{
var response = new AccountCreateResponse();
var account = new Account() { Customer = request.CustomerName };
repository.Add(account);
return response;
}
public FindAllAccountsResponse GetAllAcounts()
{
var response = new FindAllAccountsResponse();
var accounts = new List<AccountView>();
foreach(Account account in repository.FindAll())
{
accounts.Add(ViewMapper.CreateAccountView(account));
}
response.Accounts = accounts;
return response;
}
public FindAccountResponse GetAcountBy(Guid id)
{
var response = new FindAccountResponse();
var acct = repository.FindBy(id);
var account = ViewMapper.CreateAccountView(acct);
foreach (Transaction trans in acct.Transactions)
{
account.Transactions.Add(ViewMapper.CreateTransactionView(trans));
}
response.Account = account;
return response;
}
public void Deposit(DepositRequest request)
{
var account = repository.FindBy(request.AccountID);
account.Deposit(request.Amount, "");
repository.Save(account);
}
public void Withdraw(WithdrawRequest request)
{
var account = repository.FindBy(request.AccountID);
account.Withdraw(request.Amount, "");
repository.Save(account);
}
public TransferResponse Transfer(TransferRequest request)
{
var response = new TransferResponse();
try
{
service.Transfer(request.AccountIDFrom, request.AccountIDTo, request.Amount);
response.Success = true;
}
catch (InsufficientFundsException)
{
response.Message = "There is not enough funds in account: " + request.AccountIDFrom.ToString();
response.Success = false;
}
return response;
}
}
//消息(Request-Response)模式
public class AccountCreateRequest
{
public string CustomerName { get; set; }
}
public class DepositRequest
{
public Guid AccountID { get; set; }
public decimal Amount { get; set; }
}
public class WithdrawRequest
{
public Guid AccountID { get; set; }
public decimal Amount { get; set; }
}
public class TransferRequest
{
public Guid AccountIDFrom { get; set; }
public Guid AccountIDTo { get; set; }
public decimal Amount { get; set; }
}
public abstract class ResponseBase
{
/// <summary>
/// 指明被调用的方法是否成功运行
/// </summary>
public bool Success { get; set; }
/// <summary>
/// 包含该方法运行结果的详细信息
/// </summary>
public string Message { get; set; }
}
public class AccountCreateResponse : ResponseBase
{
public Guid AccountID { get; set; }
}
public class FindAllAccountsResponse : ResponseBase
{
public IList<AccountView> Accounts { get; set; }
}
public class FindAccountResponse : ResponseBase
{
public AccountView Account { get; set; }
}
public class TransferResponse : ResponseBase
{
}
DomainModel.Web 负责应用程序的表示和用户体验需求。这个项目只与Service交互,并接收专门为用户体验视图创建的强类型视图模型。
//前台部分
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="default.aspx.cs" Inherits="DomainModel.Web._default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<fieldset>
<legend>Create New Account</legend>
<p>
Customer Name:
<asp:TextBox ID="txtCustomerName" runat="server" />
<asp:Button ID="btCreateAccount" runat="server" Text="Create Account"
onclick="btCreateAccount_Click" />
</p>
</fieldset>
<fieldset>
<legend>Account Detail</legend>
<p>
<asp:DropDownList AutoPostBack="true" ID="ddlAccounts" runat="server"
onselectedindexchanged="ddlAccounts_SelectedIndexChanged">
</asp:DropDownList>
</p>
<p>
Account ID:
<asp:Label ID="lblAccountID" runat="server" />
</p>
<p>
Customer Name:
<asp:Label ID="lblCustomerName" runat="server" />
</p>
<p>
Balance:
<asp:Label ID="lblBalance" runat="server" />
</p>
<p>
Amount<asp:TextBox ID="txtAmount" runat="server" Width="60px" />
<asp:Button ID="btnWithdraw" runat="server" Text="Withdraw"
onclick="btnWithdraw_Click" />
<asp:Button ID="btnDeposit" runat="server" Text="Deposit"
onclick="btnDeposit_Click" />
</p>
<p>
Transfer
<asp:TextBox ID="txtAmountToTransfer" runat="server" Width="60px" />
to
<asp:DropDownList AutoPostBack="true" ID="ddlAccountsTransferTo" runat="server" />
<asp:Button ID="btnTransfer" runat="server" Text="Commit"
onclick="btnTransfer_Click" />
</p>
<p>
Transactions</p>
<asp:Repeater ID="rptTransactions" runat="server">
<HeaderTemplate>
<table>
<tr>
<td>
Deposit
</td>
<td>
Withdraw
</td>
<td>
Reference
</td>
<td>
TransDate
</td>
</tr>
</HeaderTemplate>
<ItemTemplate>
<tr>
<td>
<%# Eval("Deposit") %>
</td>
<td>
<%# Eval("Withdraw") %>
</td>
<td>
<%# Eval("Reference") %>
</td>
<td>
<%# Eval("TransDate") %>
</td>
</tr>
</ItemTemplate>
<FooterTemplate>
</table>
</FooterTemplate>
</asp:Repeater>
</fieldset>
</div>
</form>
</body>
</html>
//后台部分
public partial class _default : System.Web.UI.Page
{
private ApplicationAccountService service = new ApplicationAccountService();
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
ShowAllAcounts();
}
}
protected void btCreateAccount_Click(object sender, EventArgs e)
{
var customerName = this.txtCustomerName.Text.Trim();
if (customerName.IsNullData()) return;
var request = new AccountCreateRequest() { CustomerName = customerName };
service.CreateAccount(request);
ShowAllAcounts();
}
protected void ddlAccounts_SelectedIndexChanged(object sender, EventArgs e)
{
DisplayAccount();
}
protected void btnWithdraw_Click(object sender, EventArgs e)
{
var request = new WithdrawRequest()
{
AccountID = new Guid(ddlAccounts.SelectedValue.ToString()),
Amount = txtAmount.Text.ConvertToDecimal()
};
service.Withdraw(request);
DisplayAccount();
}
protected void btnDeposit_Click(object sender, EventArgs e)
{
var request = new DepositRequest()
{
AccountID = new Guid(ddlAccounts.SelectedValue.ToString()),
Amount = txtAmount.Text.ConvertToDecimal()
};
service.Deposit(request);
DisplayAccount();
}
protected void btnTransfer_Click(object sender, EventArgs e)
{
var request = new TransferRequest()
{
AccountIDFrom = new Guid(ddlAccounts.SelectedValue.ToString()),
AccountIDTo = new Guid(ddlAccountsTransferTo.SelectedValue.ToString()),
Amount = txtAmountToTransfer.Text.ConvertToDecimal()
};
service.Transfer(request);
DisplayAccount();
}
#region Private Methods
private void ShowAllAcounts()
{
ShowAllAcounts(ddlAccounts);
ShowAllAcounts(ddlAccountsTransferTo);
}
private void ShowAllAcounts(DropDownList accounts)
{
accounts.Items.Clear();
var response = service.GetAllAcounts();
accounts.Items.Add(new ListItem("Select an account", ""));
foreach (AccountView account in response.Accounts)
{
var item = new ListItem(account.Customer, account.AccountID.ToString());
if (accounts.Items.Contains(item)) continue;
accounts.Items.Add(item);
}
}
private void DisplayAccount()
{
var accountId = ddlAccounts.SelectedValue.ToString();
if (!accountId.IsNullData())
{
var response = service.GetAcountBy(new Guid(accountId));
var account = response.Account;
this.lblAccountID.Text = account.AccountID.ToString();
this.lblBalance.Text = account.Balance;
this.lblCustomerName.Text = account.Customer;
rptTransactions.DataSource = account.Transactions;
rptTransactions.DataBind();
}
}
#endregion
}
该模式可以追踪真实的领域并在领域模型中重建工作流和处理流程。与Transaction Script模式和Active Record模式相比,Domain Model模式的另一个优势是,由于它不包含数据访问代码,因此可以很容易地进行单元测试而不必模拟并隔离数据访问层所依赖的类。另外,Domain Model模式可能并不总能匹配应用程序需求。它的强大之处在于处理复杂的业务逻辑,但对于只包含非常少量业务逻辑的应用程序而言,采用一个全方位的领域模型有大材小用之嫌。该模式的另一个不足之处在于,与Active Record和Transaction Script模式相比,为了精通领域模型模式,需要面临陡峭的学习曲线。需要很多时间和经验才能高效地使用该模式,而且最重要的是,需要对正在试图建模的业务领域有全面的了解。
另外,以上代码包含一个辅助类DataConverter(包含很多扩展方法),我也将它列到这里,供大家参考。
public static class DataConverter
{
/// <summary>
/// 是否是数字
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static bool IsNumeric(this string str)
{
Regex r = new System.Text.RegularExpressions.Regex(@"^[-]?\d+[.]?\d*$");
return r.IsMatch(str);
}
/// <summary>
/// 判断是否为空数据
/// </summary>
/// <typeparam name="T">当前类型</typeparam>
/// <param name="current">当前对象</param>
/// <returns></returns>
public static bool IsNullData<T>(this T current)
{
if (current == null || (object)current == DBNull.Value) return true;
return String.IsNullOrEmpty(current.ToString().Trim());
}
/// <summary>
/// 将当前对象转化为整数(未转化成功则取默认值)
/// </summary>
/// <param name="current">当前对象</param>
/// <param name="defaultValue">默认值</param>
/// <returns></returns>
public static int ConvertToInt<T>(this T current, int defaultValue)
{
var result = 0;
return current.IsNullData() || !int.TryParse(current.ToString(), out result) ? defaultValue : result;
}
public static int ConvertToInt<T>(this T current)
{
return ConvertToInt(current, 0);
}
/// <summary>
/// 将当前对象转化为金额(未转化成功则取默认值)
/// </summary>
/// <param name="current">当前对象</param>
/// <param name="defaultValue">默认值</param>
/// <returns></returns>
public static decimal ConvertToDecimal<T>(this T current, decimal defaultValue)
{
var result = 0m;
return current.IsNullData() || !decimal.TryParse(current.ToString(), out result) ? defaultValue : result;
}
public static decimal ConvertToDecimal<T>(this T current)
{
return ConvertToDecimal(current, 0m);
}
/// <summary>
/// 将当前对象转化为字符串(未转化成功则取默认值)
/// </summary>
/// <param name="current">当前对象</param>
/// <param name="defaultValue">默认值</param>
/// <returns></returns>
public static string ConvertToString<T>(this T current, string defaultValue)
{
return current.IsNullData() ? defaultValue : current.ToString();
}
public static string ConvertToString<T>(this T current)
{
return ConvertToString(current, String.Empty);
}
/// <summary>
/// 将当前对象转化为字符串(未转化成功则取默认值)
/// </summary>
/// <param name="current">当前对象</param>
/// <param name="defaultValue">默认值</param>
/// <returns></returns>
public static DateTime ConvertToDateTime<T>(this T current, DateTime defaultValue)
{
var result = default(DateTime);
return current.IsNullData() || !DateTime.TryParse(current.ToString(), out result) ? defaultValue : result;
}
public static DateTime ConvertToDateTime<T>(this T current)
{
return ConvertToDateTime(current, DateTime.Now);
}
}
4. Anemic Domain Model(贫血领域模型): 所谓贫血领域模型,可以理解为一种与Domain Model模型相似的反模式,仍然包含表示业务领域的领域对象,只是这些对象不包含任何行为,仅作为简单的数据传输类。该模式的领域服务扮演更加过程式的代码,与Transaction Script模式比较类似,这会带来一些与之相关的问题。其中一个问题就是违背了"讲述而不要询问"原则,也就是对象应该告诉客户它们能够做什么或不能够做什么,而不是暴露属性并让客户端来决定某个对象是否处于执行给定动作所需的状态。我们再次用贫血领域模型来重建银行领域的示例,希望大家能明白其中的异同。
public class Transaction
{
public Guid Id { get; set; }
public decimal Deposit { get; set; }
public decimal Withdraw { get; set; }
public string Reference { get; set; }
public DateTime TransDate { get; set; }
public Guid AccountID { get; set; }
}
public class Account
{
public Account()
{
Transactions = new List<Transaction>();
}
public Guid AccountID { get; set; }
public decimal Balance { get; set; }
public string Customer { get; set; }
public IList<Transaction> Transactions { get; set; }
}
//单独的类被包含进来以实现逻辑
public class BankAccountHasEnoughFundsToWithdrawSpecification
{
private decimal amountToWithdraw;
public BankAccountHasEnoughFundsToWithdrawSpecification(decimal amountToWithdraw)
{
amountToWithdraw = amountToWithdraw;
}
public bool IsSatisfiedBy(Account account)
{
return account.Balance >= amountToWithdraw;
}
}
//协调提现或银行转账时,在Domain模型中创建的领域服务类将利用该规范:
public class AccountService
{
...
public void Transfer(Guid accountIdTo, Guid accountIdFrom, decimal amount)
{
var accountTo = accountRepository.FindBy(accountIdTo);
var accountFrom = accountRepository.FindBy(accountIdFrom);
var hasEnoughFunds = new BankAccountHasEnoughFundsToWithdrawSpecification(amount);
if (hasEnoughFunds.IsSatisfiedBy(accountFrom))
{
//make the transfer..
}
else
{
throw new InsufficientFundsException();
}
}
public void Withdraw(Guid accountId, decimal amount, string reference)
{
var account = accountRepository.FindBy(accountId);
var hasEnoughFunds = new BankAccountHasEnoughFundsToWithdrawSpecification(amount);
if (hasEnoughFunds.IsSatisfiedBy(account))
{
//make the withdraw..
}
else
{
throw new InsufficientFundsException();
}
}
...
}
在处理复杂业务逻辑时,Domain Model模式非常有用。而DDD(Domain-Driven Design,领域驱动设计)就是一种流行的利用Domain Model模式的设计方法学。简而言之,DDD就是一组帮助人们构建能够反映业务理解并满足业务需求的应用程序的模式和原则。除此之外,它还是一种思考开发方法学的新方法。DDD探讨对真实领域建模,首先要全面理解该领域,并将所有的术语、规则和逻辑放入到代码的抽象表示(通常是以领域模型的形式)中。