ASP.NET 设计模式 ( 组织业务逻辑层) 读书摘记4
2013-05-25 13:44 Hejin.Wong 阅读(2663) 评论(6) 编辑 收藏 举报本文地址:http://www.cnblogs.com/egger/archive/2013/03/10/2952849.html 欢迎转载 ,请保留此链接๑•́ ₃•̀๑!
理解业务组织模式
作为开发者,要理解所有领域逻辑模式的优缺点,这样才能使用最合适的模式。谨记并非所有应用程序都是一样的,也并非所有应用程序都需要复杂的体系结构来封装系统的业务逻辑。
业务层在任何企业应用程序中是最重要的层次。作为开发者,重要的是要理解所有领域逻辑模式的优缺点,这样才能使用最合适的模式。
Fowler的著作 Patterns of Enterprise Application Architecture 中首先提出的4种模式:
Transaction Script(事务脚本)、Active Record(活动记录)、Anemic Model(贫血模型)及Domain Model(领域模型)。
理解领域驱动设计(domain-driven design,DDD)以及如何运用它让自己专注于业务逻辑而不是基础设施关注点.学习DDD,这种设计方法有助于更有效地理解正在建模的业务领域并确保牢记业务需求。
Transaction Script
Transaction Script模式遵循的是过程式开发风格而不是面向对象方法。通常,为每个业务事务创建一个单独的方法,并将它们组合起来放入某种静态管理程序或服务类。每个过程都包含了完成业务事务所需的所有业务逻辑,包括工作流、业务规则和数据库持久化验证检查。
Transaction Script模式的一个优势是它易于理解,很快就可以让团队新成员上手而不需要具有该模式的预备知识。当出现新需求时,很容易向该类中添加更多方法,而不用担心影响或破坏现有功能。因此该模式非常适于那些逻辑中只包含很少或不包含可能增长的功能集合的小型应用程序,以及有不熟悉面向对象编程概念的初级开发者的团队。
当应用程序变大而且业务逻辑变得更复杂时,Transaction Script模式的问题就会暴露出来。扩展应用程序时,方法的数目也会变多,从而形成一个充斥着功能交叠的细粒度方法的无用API。尽管可以使用子方法来避免代码重复,如验证和业务规则,但在工作流中的复制不可避免,而且当应用程序规模变大时,代码基很快会变得笨重且不可维护。
应用程序较小,而且业务逻辑简单,不需要采用完全的面向对象方法,Transaction Script模式可能比较合适。如果应用程序规模会变大,那就可能需要重新考虑业务逻辑结构并寻求更具伸缩性的模式,如Active Record模式,
Active Record
Active Record模式是一种流行的模式,尤其在底层数据库模型匹配业务模型时它特别有效。通常,数据库中的每张表都对应一个业务对象。业务对象表示表中的一行,并且包含数据、行为以及持久化该对象的工具,此外还有添加新实例和查找对象集合所需的方法。
图展示一个博客应用程序中的Post和Comment对象如何与它们对应的数据库表关联起来。该图还说明Post中含有一个Comment对象集合。
在Active Record模式中,每个业务对象均负责自己的持久化和相关的业务逻辑。
Active Record模式非常适用于在数据模型和业务模型之间具有一对一映射关系的简单应用程序,如博客或论坛引擎。如果已经有数据库模型或者希望采用“数据优先”的方法来构建应用程序,这也是一个可用的好模式。因为业务对象与数据库中的表具有一对一映射关系,而且均具有相同的创建、读取、更新和删除(CRUD)方法,所以可以使用代码生成工具自动生成业务模型。优秀的代码生成工具还会内置所有的数据库验证逻辑,以确保只有有效的数据才会持久化。
Active Record模式随着基于数据库的Web应用程序而流行,其中一个典型就是结合了MVC模式和Active Record ORM的Ruby on Rails框架。在.NET领域,构建在NHibernate之上的Castle ActiveRecord项目是最流行的开放源代码Active Record框架之一。
Castle ActiveRecord(官网)框架是一个基于.NET的ORM框架,它实现了ActiveRecord设计模式。它本身就是基于NHibernate,只是封 装了NHibernate的大部分烦杂细节,对于需要持久化的类,只需继承自ActiveRecordBase类,并对类中的property赋予正确的 Attribute,而无需编写烦杂的mapping file。对于大型系统复杂的数据库逻辑,Castle project建议仍然使用NHibernate作为ORM,而对于数据库数据大批量的迁移、备份等操作,Castle project建议不采用任何ORM机制,而直接使用ADO.NET。
看到上面的描述,经常使用EF框架的朋友就会感觉很熟悉。.NET中,ADO.NET Entity Framework 是微软以 ADO.NET 为基础所发展出来的对象关系对应 (O/R Mapping) 解决方案。微软MVC3吸收了Ruby on Rails的特点,使用MVCScaffolding +EF CodeFirs,就会体验Ruby On Rails一样创建代码结构的快感。
Active Record模式擅长于处理底层数据模型能够很好映射到业务模型的情形,但是当出现不匹配时(有时候称为阻抗失配),该模式将很难应对。这是由于复杂系统的概念业务模型有时与数据模型不同所造成的。如果业务领域非常丰富,有着大量的复杂规则、逻辑和工作流,那么采用Domain Model方法将更加有利。
Domain Model
在处理复杂业务逻辑时,Domain Model模式非常有用.Domain Model视为表示正在处理的领域的概念层。事物以及事物之间的关系都存在于这个模型中。这些事物包含数据,更重要的是它们还有行为。Domain Model越能密切地表示真实的领域越好,这是因为更容易理解和复制组织中的复杂的业务逻辑、规则和验证过程。
Domain Model与Active Record模式之间的主要差别在于,Domain Model中存在的业务实体并不知道如何持久化自己,而且没有必要在数据模型和业务模型之间建立一对一的映射关系。
POCO和PI
术语持久化不知(persistence ignorance,PI)表示普通CLR对象(plain old common runtime object,POCO)业务实体的朴实本质。那么如何将Domain Model的业务对象持久化呢?通常使用Repository模式。采用Domain Model模式时,Repository对象以及数据映射器负责将业务实体及相关实体的对象图映射到数据模型。
领域驱动设计 DDD
DDD(Domain-Driven Design,领域驱动设计)就是一种流行的利用Domain Model模式的设计方法学。
DDD就是一组帮助人们构建能够反映业务理解并满足业务需求的应用程序的模式和原则。DDD探讨对真实领域建模,首先要全面理解该领域,并将所有的术语、规则和逻辑放入到代码的抽象表示(通常是以领域模型的形式)中。
1. 通用语言
通用语言(ubiquitous language)的概念是,它应该充当一个公共词汇表,开发者、领域专家及任何其他参与项目的人都使用它来描述该领域。领域专家具有特定领域知识和技能,并且在开发领域模型的过程中与您密切协作,以确保在尝试使用代码表示业务模型之前完全理解该模型。所编写的类、方法和属性名称都应该基于同样的通用语言。这可以让您使用领域专家能够理解的语言来谈论代码。此外,接触该代码的新开发者也应该能够了解该领域。它还让他们能够以相对容易的方式与业务专家谈论复杂业务逻辑的哪怕是最细微的细节。当参与应用程序开发的各方都使用相同的语言时,人们就可以容易地表达问题和解决方法,从而让应用程序更快、更容易地构建。DDD并不是一个框架,但它确实有一组构建块或概念可供整合到解决方案中。
2. 实体
实体就是以一种抽象的方式包含了真实实体中的数据和行为。任何与实体相关的逻辑都应该包含在它内部。实体属于需要标识符的事物,在其整个生命周期中,该标识符都将保持不变。通常,系统使用某种唯一标识符或自动编号值来为所有无法采用自然方式标识的实体提供标识符。有时候,实体确实具有自然键,如社会保险号或员工号码。并不是领域模型中的所有对象都是唯一的而且需要标识。对于某些对象,数据是最重要的,而不是标识。这些对象就被称为值对象。
3. 值对象
值对象没有标识,它们之所以重要只是因为它们的特性。值对象通常并不会单独存在,它们通常是(但并非总是)实体的属性。
4. 聚合和聚合根
大型系统或复杂领域可能有成百上千的实体和值对象,它们有着错综复杂的关系。领域模型需要一种方法来管理这些关联,更重要的是,在逻辑上属于同一分组的实体和值对象需要定义一个接口,让其他实体能够通过该接口与它们交互。如果没有这类构造,那么以后不同分组对象之间的交互将会相互干扰并产生问题。
聚合概念将逻辑实体和值对象分组。根据DDD的定义,聚合只是"一族出于数据变化目的而被视作一个单元的相关联对象"。聚合根是一个实体,它是这个聚合中唯一能够允许聚合外的对象持有引用的成员。DDD中的聚合概念是为了确保领域模型中的数据完整性。聚合根是一个充当进入聚合的逻辑途径的特殊实体。例如,如果在电子商务商店上下文中获取一张订单,那么可以将其视为聚合根,因为我们只希望通过访问聚合的根来编辑订单项或应用一张凭证。这使得复杂对象图能够保持一致,而且能够遵守业务规则。因此,与其让一个订单对象通过简单的List属性来暴露它发出的凭证集合,不如让它拥有一些带有复杂规则的方法,能够允许将凭证应用到它并且把凭证列表表现为一个用于显示的只读集合。
5. 领域服务
那些没有真正位于单个实体中或者需要访问资源库的方法都被放到领域服务中。领域服务层还可以包含自己的领域逻辑,而且可以作为领域模型的重要组成部分,像实体和值对象一样。
6. 应用程序服务
应用程序服务是位于领域模型之上的一个瘦层,负责协调应用程序活动。它并不包含业务逻辑,也没有保存任何实体的状态。但它可以存放业务工作流事务的状态。
7. 资源库
Repository模式充当业务实体的内存集合或仓库,它完全将底层的数据基础设施抽象出来。该模式可用来将领域模型与任何基础设施关注点分离,使其成为POCO和PI。
8. 分层
在DDD中,分层是一种重要的概念,因为它有助于加强关注点的分离。下图所示为构成DDD的各个层次和概念的图形化表示。
推荐阅读:
Domain-Driven Design: Tackling Complexity in the Heart of Software(Addison-Wesley,2003),Eric Evans著
Applying Domain-Driven Design and Patterns: Using .Net With Examples in C# and .NET (Addison-Wesley,2006),Jimmy Nilsson著
示例讲解
下面将通过创建一个解决方案来为银行领域建模,这涉及账号的创建以及账号之间的现金转账。来演示Domain Model模式。(示例源码下载)
创建一个的新的解决方案,并向其中添加下面的项目:
ASPPatterns.Chap4.DomainModel.UI.Web为Web应用程序,其余皆为类库项目。在Repository项目上右击,并添加对Model项目的项目引用。在AppService项目上右击,添加对Model和Repository项目的项目引用。最后,在Web项目上右击,添加对AppService项目的项目引用。
下面为项目的解决方案结构和每个对象的责任。
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交互,并接收专门为用户体验视图创建的强类型视图模型。
使用下面的数据库脚本创建数据库及相关表:
USE [BankAccout] GO /****** Object: Table [dbo].[Transactions] Script Date: 05/25/2013 13:52:33 ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TABLE [dbo].[Transactions]( [BankAccountId] [uniqueidentifier] NOT NULL, [Deposit] [money] NOT NULL, [Withdrawal] [money] NOT NULL, [Reference] [nvarchar](50) NOT NULL ) ON [PRIMARY] GO /****** Object: Table [dbo].[BankAccounts] Script Date: 05/25/2013 13:52:33 ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TABLE [dbo].[BankAccounts]( [BankAccountId] [uniqueidentifier] NOT NULL, [Balance] [money] NOT NULL, [CustomerRef] [nvarchar](50) NOT NULL, CONSTRAINT [PK_BankAccouts] PRIMARY KEY CLUSTERED ( [BankAccountId] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO
在搭建好解决方案框架和数据库之后,就可以真正开始领域建模。在这个场景中,BankAccount为发生的每个动作创建一个Transaction对象。下图给出了这个简单领域模型的类图。
在Model项目中创建一个新类Transaction,Transaction对象并没有标识符(identifier)属性,而对应的数据表没有指定主键。Transaction对象被称为值对象。
1 public class Transaction 2 { 3 public Transaction(decimal deposit, decimal withdrawal, string reference, DateTime date) 4 { 5 this.Deposit = deposit; 6 this.Withdrawal = withdrawal; 7 this.Reference = reference; 8 this.Date = date; 9 } 10 11 public decimal Deposit 12 { get; internal set; } 13 14 public decimal Withdrawal 15 { get; internal set; } 16 17 public string Reference 18 { get; internal set; } 19 20 public DateTime Date 21 { get; internal set; } 22 }
在Model项目中创建类BankAccount。
1 public class BankAccount 2 { 3 private decimal _balance; 4 private Guid _accountNo; 5 private string _customerRef; 6 private IList<Transaction> _transactions; 7 8 public BankAccount() : this(Guid.NewGuid(), 0, new List<Transaction>(), "") 9 { 10 _transactions.Add(new Transaction(0m, 0m, "account created", DateTime.Now)); 11 } 12 13 public BankAccount(Guid Id, decimal balance, IList<Transaction> transactions, string customerRef) 14 { 15 AccountNo = Id; 16 _balance = balance; 17 _transactions = transactions; 18 _customerRef = customerRef; 19 } 20 21 public Guid AccountNo 22 { 23 get { return _accountNo; } 24 internal set { _accountNo = value; } 25 } 26 27 public decimal Balance 28 { 29 get { return _balance; } 30 internal set { _balance = value; } 31 } 32 33 public string CustomerRef 34 { 35 get { return _customerRef; } 36 set { _customerRef = value; } 37 } 38 39 public bool CanWithdraw(decimal amount) 40 { 41 return (Balance >= amount); 42 } 43 44 public void Withdraw(decimal amount, string reference) 45 { 46 if (CanWithdraw(amount)) 47 { 48 Balance -= amount; 49 _transactions.Add(new Transaction(0m, amount, reference, DateTime.Now)); 50 } 51 else 52 { 53 throw new InsufficientFundsException(); 54 } 55 } 56 57 public void Deposit(decimal amount, string reference) 58 { 59 Balance += amount; 60 _transactions.Add(new Transaction(amount, 0m, reference, DateTime.Now)); 61 } 62 63 public IEnumerable<Transaction> GetTransactions() 64 { 65 return _transactions; 66 } 67 }
在Model项目中添加一个自定义异常类InsufficientFundsException。
public class InsufficientFundsException : ApplicationException { }
因为不希望污染Domain Model项目,所以向model中添加Repository的接口来定义契约,以满足实体BankAccount和Transactions的持久化和检索需求。
namespace ASPPatterns.Chap4.DomainModel.Model { public interface IBankAccountRepository { void Add(BankAccount bankAccount); void Save(BankAccount bankAccount); IEnumerable<BankAccount> FindAll(); BankAccount FindBy(Guid AccountId); } }
有些动作没有很好地映射到领域实体的方法。对于这类情况,可以使用领域服务。在两个账号之间转账的动作就是一种属于服务类的责任。向Model项目中添加一个名为BankAccountService的新类:
1 public class BankAccountService 2 { 3 private IBankAccountRepository _bankAccountRepository; 4 5 public BankAccountService(IBankAccountRepository bankAccountRepository) 6 { 7 _bankAccountRepository = bankAccountRepository; 8 } 9 10 public void Transfer(Guid accountNoTo, Guid accountNoFrom, decimal amount) 11 { 12 BankAccount bankAccountTo = _bankAccountRepository.FindBy(accountNoTo); 13 BankAccount bankAccountFrom = _bankAccountRepository.FindBy(accountNoFrom); 14 15 if (bankAccountFrom.CanWithdraw(amount)) 16 { 17 bankAccountTo.Deposit(amount, "From Acc " + bankAccountFrom.CustomerRef + " "); 18 bankAccountFrom.Withdraw(amount, "Transfer To Acc " + bankAccountTo.CustomerRef + " "); 19 20 _bankAccountRepository.Save(bankAccountTo); 21 _bankAccountRepository.Save(bankAccountFrom); 22 } 23 else 24 { 25 throw new InsufficientFundsException(); 26 } 27 } 28 }
领域模型已构建完毕,那么可以编写方法来持久化Bank Account和Transaction业务对象。在Repository项目中,添加一个新类BankAccountRepository,用来持久化和检索需求(使用ADO.NET)。该类将实现接口IBankAccountRepository。
1 namespace ASPPatterns.Chap4.DomainModel.Repository 2 { 3 public class BankAccountRepository : IBankAccountRepository 4 { 5 private string _connectionString; 6 7 public BankAccountRepository() 8 { 9 _connectionString = ConfigurationManager.ConnectionStrings["BankAccountConnectionString"].ConnectionString; 10 } 11 12 public void Add(BankAccount bankAccount) 13 { 14 string insertSql = "INSERT INTO BankAccounts " + 15 "(BankAccountID, Balance, CustomerRef) VALUES " + 16 "(@BankAccountID, @Balance, @CustomerRef)"; 17 18 using (SqlConnection connection = 19 new SqlConnection(_connectionString)) 20 { 21 SqlCommand command = connection.CreateCommand(); 22 command.CommandText = insertSql; 23 24 SetCommandParametersForInsertUpdateTo(bankAccount, command); 25 26 connection.Open(); 27 28 command.ExecuteNonQuery(); 29 } 30 31 UpdateTransactionsFor(bankAccount); 32 } 33 34 public void Save(BankAccount bankAccount) 35 { 36 string bankAccoutnUpdateSql = "UPDATE BankAccounts " + 37 "SET Balance = @Balance, CustomerRef= @CustomerRef " + 38 "WHERE BankAccountID = @BankAccountID;"; 39 40 using (SqlConnection connection = 41 new SqlConnection(_connectionString)) 42 { 43 SqlCommand command = connection.CreateCommand(); 44 command.CommandText = bankAccoutnUpdateSql; 45 46 SetCommandParametersForInsertUpdateTo(bankAccount, command); 47 48 connection.Open(); 49 50 command.ExecuteNonQuery(); 51 } 52 53 UpdateTransactionsFor(bankAccount); 54 } 55 56 private static void SetCommandParametersForInsertUpdateTo(BankAccount bankAccount, SqlCommand command) 57 { 58 command.Parameters.Add(new SqlParameter("@BankAccountID", bankAccount.AccountNo)); 59 command.Parameters.Add(new SqlParameter("@Balance", bankAccount.Balance)); 60 command.Parameters.Add(new SqlParameter("@CustomerRef", bankAccount.CustomerRef)); 61 } 62 63 private void UpdateTransactionsFor(BankAccount bankAccount) 64 { 65 string deleteTransactionSQl = "DELETE Transactions WHERE BankAccountId = @BankAccountId;"; 66 67 using (SqlConnection connection = 68 new SqlConnection(_connectionString)) 69 { 70 SqlCommand command = connection.CreateCommand(); 71 command.CommandText = deleteTransactionSQl; 72 command.Parameters.Add(new SqlParameter("@BankAccountID", bankAccount.AccountNo)); 73 connection.Open(); 74 command.ExecuteNonQuery(); 75 76 } 77 78 string insertTransactionSql = "INSERT INTO Transactions " + 79 "(BankAccountID, Deposit, Withdraw, Reference, [Date]) VALUES " + 80 "(@BankAccountID, @Deposit, @Withdraw, @Reference, @Date)"; 81 82 foreach (Transaction tran in bankAccount.GetTransactions()) 83 { 84 using (SqlConnection connection = 85 new SqlConnection(_connectionString)) 86 { 87 SqlCommand command = connection.CreateCommand(); 88 command.CommandText = insertTransactionSql; 89 90 command.Parameters.Add(new SqlParameter("@BankAccountID", bankAccount.AccountNo)); 91 command.Parameters.Add(new SqlParameter("@Deposit", tran.Deposit)); 92 command.Parameters.Add(new SqlParameter("@Withdraw", tran.Withdrawal)); 93 command.Parameters.Add(new SqlParameter("@Reference", tran.Reference)); 94 command.Parameters.Add(new SqlParameter("@Date", tran.Date)); 95 96 connection.Open(); 97 command.ExecuteNonQuery(); 98 } 99 } 100 } 101 102 public IEnumerable<BankAccount> FindAll() 103 { 104 IList<BankAccount> accounts = new List<BankAccount>(); 105 106 string queryString = "SELECT * FROM dbo.Transactions INNER JOIN " + 107 "dbo.BankAccounts ON dbo.Transactions.BankAccountId = dbo.BankAccounts.BankAccountId " + 108 "ORDER BY dbo.BankAccounts.BankAccountId;"; 109 110 using (SqlConnection connection = 111 new SqlConnection(_connectionString)) 112 { 113 SqlCommand command = connection.CreateCommand(); 114 command.CommandText = queryString; 115 116 connection.Open(); 117 118 using (SqlDataReader reader = command.ExecuteReader()) 119 { 120 accounts = CreateListOfAccountsFrom(reader); 121 } 122 } 123 124 return accounts; 125 } 126 127 private IList<BankAccount> CreateListOfAccountsFrom(IDataReader datareader) 128 { 129 IList<BankAccount> accounts = new List<BankAccount>(); 130 BankAccount bankAccount; 131 string id = ""; 132 IList<Transaction> transactions = new List<Transaction>(); 133 134 while (datareader.Read()) 135 { 136 if (id != datareader["BankAccountId"].ToString()) 137 { 138 id = datareader["BankAccountId"].ToString(); 139 transactions = new List<Transaction>(); 140 bankAccount = new BankAccount(new Guid(id), Decimal.Parse(datareader["Balance"].ToString()), transactions, datareader["CustomerRef"].ToString()); 141 accounts.Add(bankAccount); 142 } 143 transactions.Add(CreateTransactionFrom(datareader)); 144 } 145 146 return accounts; 147 } 148 149 private Transaction CreateTransactionFrom(IDataRecord rawData) 150 { 151 return new Transaction(Decimal.Parse(rawData["Deposit"].ToString()), 152 Decimal.Parse(rawData["Withdraw"].ToString()), 153 rawData["Reference"].ToString(), 154 DateTime.Parse(rawData["Date"].ToString())); 155 } 156 157 158 public BankAccount FindBy(Guid AccountId) 159 { 160 BankAccount account; 161 162 string queryString = "SELECT * FROM dbo.Transactions INNER JOIN " + 163 "dbo.BankAccounts ON dbo.Transactions.BankAccountId = dbo.BankAccounts.BankAccountId " + 164 "WHERE dbo.BankAccounts.BankAccountId = @BankAccountId;"; 165 166 using (SqlConnection connection = 167 new SqlConnection(_connectionString)) 168 { 169 SqlCommand command = connection.CreateCommand(); 170 command.CommandText = queryString; 171 172 SqlParameter Idparam = new SqlParameter("@BankAccountId", AccountId); 173 command.Parameters.Add(Idparam); 174 175 connection.Open(); 176 177 using (SqlDataReader reader = command.ExecuteReader()) 178 { 179 account = CreateListOfAccountsFrom(reader)[0]; 180 } 181 } 182 return account; 183 } 184 } 185 }
下面对服务层进行编码,在AppServices项目中添加一个新文件夹ViewModel,并向该文件夹中添加两个新类BankAccountView和TransactionView,提供了领域模型用于表示的展开视图(让人想起了MVC中的viewmodel)。
1 public class TransactionView 2 { 3 public string Deposit { get; set; } 4 public string Withdrawal { get; set; } 5 public string Reference { get; set; } 6 public DateTime Date { get; set; } 7 } 8 public class BankAccountView 9 { 10 public Guid AccountNo { get; set; } 11 public string Balance { get; set; } 12 public string CustomerRef { get; set; } 13 public IList<TransactionView> Transactions { get; set; } 14 }
创建一个包含两个静态方法的新映射器类ViewMapper,用来将领域实体转换成数据传输视图模型。
1 public static class ViewMapper 2 { 3 public static TransactionView CreateTransactionViewFrom(Transaction tran) 4 { 5 return new TransactionView 6 { 7 Deposit = tran.Deposit.ToString("C"), 8 Withdrawal = tran.Withdrawal.ToString("C"), 9 Reference = tran.Reference, 10 Date = tran.Date 11 }; 12 } 13 14 public static BankAccountView CreateBankAccountViewFrom(BankAccount acc) 15 { 16 return new BankAccountView 17 { 18 AccountNo = acc.AccountNo, 19 Balance = acc.Balance.ToString("C"), 20 CustomerRef = acc.CustomerRef, 21 Transactions = new List<TransactionView>() 22 }; 23 } 24 }
向AppServices项目中再添加一个文件夹Messages,该文件夹将包含所有用来与服务层通信的请求-应答对象。
1 public abstract class ResponseBase 2 { 3 public bool Success { get; set; } 4 public string Message { get; set; } 5 } 6 public class BankAccountCreateResponse : ResponseBase 7 { 8 public Guid BankAccountId { get; set; } 9 } 10 public class BankAccountCreateRequest 11 { 12 public string CustomerName { get; set; } 13 } 14 public class DepositRequest 15 { 16 public Guid AccountId { get; set; } 17 public decimal Amount { get; set; } 18 } 19 public class TransferResponse : ResponseBase 20 { 21 } 22 public class TransferRequest 23 { 24 public Guid AccountIdTo { get; set; } 25 public Guid AccountIdFrom { get; set; } 26 public decimal Amount { get; set; } 27 } 28 public class WithdrawalRequest 29 { 30 public Guid AccountId { get; set; } 31 public decimal Amount { get; set; } 32 } 33 public class FindBankAccountResponse : ResponseBase 34 { 35 public BankAccountView BankAccount { get; set; } 36 } 37 public class FindAllBankAccountResponse : ResponseBase 38 { 39 public IList<BankAccountView> BankAccountView { get; set; } 40 }
在AppService项目的根目录下面添加一个新类ApplicationBankAccountService用来协调应用程序活动并将所有的业务任务委托给领域模型.
1 public class ApplicationBankAccountService 2 { 3 private BankAccountService _bankAccountService; 4 private IBankAccountRepository _bankRepository; 5 6 public ApplicationBankAccountService() : 7 this (new BankAccountRepository(), new BankAccountService(new BankAccountRepository())) 8 { } 9 10 public ApplicationBankAccountService(IBankAccountRepository bankRepository, BankAccountService bankAccountService) 11 { 12 _bankRepository = bankRepository; 13 _bankAccountService = bankAccountService; 14 } 15 16 public ApplicationBankAccountService(BankAccountService bankAccountService, IBankAccountRepository bankRepository) 17 { 18 _bankAccountService = bankAccountService; 19 _bankRepository = bankRepository; 20 } 21 22 public BankAccountCreateResponse CreateBankAccount(BankAccountCreateRequest bankAccountCreateRequest) 23 { 24 BankAccountCreateResponse bankAccountCreateResponse = new BankAccountCreateResponse(); 25 BankAccount bankAccount = new BankAccount(); 26 27 bankAccount.CustomerRef = bankAccountCreateRequest.CustomerName; 28 _bankRepository.Add(bankAccount); 29 30 bankAccountCreateResponse.BankAccountId = bankAccount.AccountNo; 31 bankAccountCreateResponse.Success = true; 32 33 return bankAccountCreateResponse; 34 } 35 36 public void Deposit(DepositRequest depositRequest) 37 { 38 BankAccount bankAccount = _bankRepository.FindBy(depositRequest.AccountId); 39 40 bankAccount.Deposit(depositRequest.Amount, ""); 41 42 _bankRepository.Save(bankAccount); 43 } 44 45 public void Withdrawal(WithdrawalRequest withdrawalRequest) 46 { 47 BankAccount bankAccount = _bankRepository.FindBy(withdrawalRequest.AccountId); 48 49 bankAccount.Withdraw(withdrawalRequest.Amount, ""); 50 51 _bankRepository.Save(bankAccount); 52 } 53 54 public TransferResponse Transfer(TransferRequest request) 55 { 56 TransferResponse response = new TransferResponse(); 57 58 try 59 { 60 _bankAccountService.Transfer(request.AccountIdTo, request.AccountIdFrom, request.Amount); 61 response.Success = true; 62 } 63 catch (InsufficientFundsException) 64 { 65 response.Message = "There is not enough funds in account no: " + request.AccountIdFrom.ToString(); 66 response.Success = false; 67 } 68 69 return response; 70 } 71 72 public FindAllBankAccountResponse GetAllBankAccounts() 73 { 74 FindAllBankAccountResponse FindAllBankAccountResponse = new FindAllBankAccountResponse(); 75 IList<BankAccountView> bankAccountViews = new List<BankAccountView>(); 76 FindAllBankAccountResponse.BankAccountView = bankAccountViews; 77 78 foreach (BankAccount acc in _bankRepository.FindAll()) 79 { 80 bankAccountViews.Add(ViewMapper.CreateBankAccountViewFrom(acc)); 81 } 82 83 return FindAllBankAccountResponse; 84 } 85 86 public FindBankAccountResponse GetBankAccountBy(Guid Id) 87 { 88 FindBankAccountResponse bankAccountResponse = new FindBankAccountResponse(); 89 BankAccount acc = _bankRepository.FindBy(Id); 90 BankAccountView bankAccountView = ViewMapper.CreateBankAccountViewFrom(acc); 91 92 foreach (Transaction tran in acc.GetTransactions()) 93 { 94 bankAccountView.Transactions.Add(ViewMapper.CreateTransactionViewFrom(tran)); 95 } 96 97 bankAccountResponse.BankAccount = bankAccountView; 98 99 return bankAccountResponse; 100 } 101 102 }
服务层并不包含任何业务逻辑,有助于防止任何与业务无关的代码污染领域模型项目。该层还将领域实体转换成数据传输对象,从而保护领域的内部操作,并为一起工作的表示层提供了一个易于使用的API。
UI层代码请下载项目源码进行查看。
使用Domain Model模式时,首先为真实的业务模型创建一个抽象的模型。有了这个模型之后,就可以对复杂逻辑进行建模:追踪真实的领域并在领域模型中重建工作流和处理流程。与Transaction Script模式和Active Record模式相比,Domain Model模式的另一个优势是,由于它不包含数据访问代码,因此可以很容易地进行单元测试而不必模拟并隔离数据访问层所依赖的类。精通领域模型模式,需要面临陡峭的学习曲线。需要很多时间和经验才能高效地使用该模式,
[示例源码下载]
Anemic Domain Model
Anemic Domain Model有时候被称为一种反模式。该模式与Domain Model模式非常类似,仍会找到表示业务领域的领域对象。但这些领域对象中不包含任何行为。相反,这些行为位于模型之外,而让领域对象作为简单的数据传输类。这种模式的主要不足之处在于,领域服务扮演更加过程式的代码,与本章开头看过的Transaction Script模式比较类似,这会带来一些与之相关的问题。其中一个问题就是违背了"讲述而不要询问"原则,也就是对象应该告诉客户它们能够做什么或不能够做什么,而不是暴露属性并让客户端来决定某个对象是否处于执行给定动作所需的状态。
上面示例中领域对象Transaction和BankAccount的逻辑现在被剥离出来,它们只是数据容器。
public class BankAccount { public BankAccount() { Transactions = new List<Transaction>(); } public Guid AccountNo { get; set; } public decimal Balance { get; set; } public string CustomerRef { get; set; } public IList<Transaction> Transactions { get; set; } } 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 Date{ get; set; } public Guid BankAccountId{ get; set; } }
可以采用Specification模式来判断某个账号是否有足够的金额来完成提现。
public class BankAccountHasEnoughFundsToWithdrawSpecification { private decimal _amountToWithdraw; public BankAccountHasEnoughFundsToWithdrawSpecification(decimal amountToWithdraw) { _amountToWithdraw = amountToWithdraw; } public bool IsSatisfiedBy(BankAccount bankAccount) { return bankAccount.Balance >= _amountToWithdraw; } }
BankAccountService类
1 public class BankAccountService 2 { 3 private IBankAccountRepository _bankAccountRepository; 4 5 public BankAccountService(IBankAccountRepository bankAccountRepository) 6 { 7 _bankAccountRepository = bankAccountRepository; 8 } 9 10 public BankAccount CreateBankAccount(string CustomerName) 11 { 12 BankAccount bankAccount = new BankAccount(); 13 14 bankAccount.AccountNo = Guid.NewGuid(); 15 bankAccount.CustomerRef = CustomerName; 16 17 Transaction openingTransaction = TransactionFactory.CreateDepositTransactionFrom(bankAccount, 0, "account created"); 18 19 bankAccount.Transactions.Add(openingTransaction); 20 21 _bankAccountRepository.Add(bankAccount); 22 23 return bankAccount; 24 } 25 26 public void Transfer(Guid accountNoTo, Guid accountNoFrom, decimal amount) 27 { 28 BankAccount bankAccountTo = _bankAccountRepository.FindBy(accountNoTo); 29 BankAccount bankAccountFrom = _bankAccountRepository.FindBy(accountNoFrom); 30 31 BankAccountHasEnoughFundsToWithdrawSpecification HasEnoughFunds = new BankAccountHasEnoughFundsToWithdrawSpecification(amount); 32 33 if (HasEnoughFunds.IsSatisfiedBy(bankAccountFrom)) 34 { 35 bankAccountTo.Balance += amount; 36 Transaction transDeposit = TransactionFactory.CreateDepositTransactionFrom(bankAccountTo, amount, "From Acc " + bankAccountFrom.CustomerRef + " "); 37 bankAccountTo.Transactions.Add(transDeposit); 38 39 bankAccountFrom.Balance -= amount; 40 Transaction transWithdraw = TransactionFactory.CreateWithdrawTransactionFrom(bankAccountFrom, amount, "Transfer To Acc " + bankAccountTo.CustomerRef + " "); 41 bankAccountFrom.Transactions.Add(transWithdraw); 42 43 _bankAccountRepository.Save(bankAccountTo); 44 _bankAccountRepository.Save(bankAccountFrom); 45 } 46 else 47 { 48 throw new InsufficientFundsException(); 49 } 50 } 51 52 public void Withdraw(Guid accountNo, decimal amount, string reference) 53 { 54 BankAccount bankAccount = _bankAccountRepository.FindBy(accountNo); 55 56 BankAccountHasEnoughFundsToWithdrawSpecification HasEnoughFunds = new BankAccountHasEnoughFundsToWithdrawSpecification(amount); 57 58 if (HasEnoughFunds.IsSatisfiedBy(bankAccount)) 59 { 60 bankAccount.Balance -= amount; 61 Transaction transWithdraw = TransactionFactory.CreateWithdrawTransactionFrom(bankAccount, amount, reference); 62 bankAccount.Transactions.Add(transWithdraw); 63 64 _bankAccountRepository.Save(bankAccount); 65 } 66 } 67 68 public void Deposit(Guid accountNo, decimal amount, string reference) 69 { 70 BankAccount bankAccount = _bankAccountRepository.FindBy(accountNo); 71 72 bankAccount.Balance += amount; 73 Transaction transDeposit = TransactionFactory.CreateDepositTransactionFrom(bankAccount, amount, reference); 74 bankAccount.Transactions.Add(transDeposit); 75 76 _bankAccountRepository.Save(bankAccount); 77 } 78 }
其他参考:
1.单一入口、MVC、ORM、CURD、ActiveRecord概念
2.http://book.51cto.com/art/201111/303307.htm