背景介绍领域对象,在此特指充血的领域对象模型,在解决什么是伪领域对象之前,需要事先解释何为充血的领域对象。在此后的介绍中,假设我们存在对象模型Employee—Department
。íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi在面向对象的实体类建模的发展历史上,有着2
家分歧,其中部分人认为实体类应保证本身的纯洁性,只需维护数据,而无需知道数据的来源以及数据的查询方法,这被称为“贫血”模型,在此模型下,一个Department
的表示如下íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiclass DepartmentíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
{íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
public string NameíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
{íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
get;íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
set;íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
}íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
public decimal BaseSalaryíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
{íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
get;íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
set;íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
}íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
}
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi其中Department
只维护了属于自己的属性“名称”和“基础工资”,而完全不需要知道属于他的Employee
的存在,对于Employee
和Department
的查询会交给专门的数据访问类(DAO
模式)
如IDepartmentDao
,此接口可能表述如下íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiinterface IDepartmentDaoíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
{íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
voidíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
Insert(Department department);íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
DepartmentíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
GetByName(string name);íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
}
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi贫血模型的优点是将实体与实体的数据访问完全解耦,实体可以在系统各层之间穿越,仅保留最基本的属性,而对实体的数据访问操作全部交由Dao
对象,这样Dao
对象可以由多态性支持,使用各种模式提供灵活性,并且符合面向对象的职责单一原则。íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi而另一部分人推荐的“充血”模型则认为“部门应当保留有自己的员工信息”,所以在实体类中应该带有相应的查询的方法,从而可以更接近现实世界地对系统进行建模,在充血模型下,Department
的表示可能如下íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiclass DepartmentíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
{íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
public string NameíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
{íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
get;íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
set;íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
}íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
public decimal BaseSalaryíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
{íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
get;íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
set;íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
}íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
public IList<Employee>íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
GetEmployees();íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
}
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi在实体类中加入的数据访问的方法,因此调用的体验有所改进,建模也更接近现实世界。íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi“充血”模型有着其固有的优势,但也无法回避一些问题,最大的问题是实体与数据访问的双向循环依赖,在没有框架支持的情况下无法做到透明地将数据访问的实现注入到实体中,同时循环依赖也是设计中的大忌,因此“充血”模型并没有得到广泛的应用。íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi可靠性考虑充血模型下的领域对象与普通的实体对象最大的区别在于,对象带有数据查询的方法。íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi但是要使对象可以进行数据的查询,就需要让对象依赖于数据层,这不是一个好的设计。幸而.NET 3.5
框架提供了一个非常便利的功能,可以动态地为对象“扩展”功能,这就是扩展方法。íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi对于扩展方法,已经有不少文章作了介绍,简单来说,扩展方法就是静态方法,通过编译期的特殊手法将此“作为”对象的实例方法进行调用。íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi由于扩展方法是静态方法,静态方法本身不支持面向对象中的多态性,无法响应变化,因此在使用扩展方法的时候,必须保证方法的调用过程没有变化,不需要由调用者显式地决定调用的途径,比如说以下方法显然不能作为静态方法使用íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiIUserDAL dal = new SqlServerUserDAL();
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi这一段代码显式地指定了IUserDAL
的实现,因此当我们的数据库产品从SqlServer
更换为Oracle
时,由于没有多态性的支持,就需要显式地修改此语句,这并不符合面向对象的开闭原则。íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi对于变化的封装,可以用工厂模式、反射等方法完成,最终以一个门面类对外提供粗粒度的接口。门面类往往是一个层的整体代表,其提供的接口稳定、变化少,在下层完成了较好的封闭的情况下,将门面类作为静态类使用,我称之为“静态门面”。íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi通过此门面类提供扩展方法,即可以将方法“注入”到实体对象中。íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi设计及实现在普通的三层架构中,业务层的方法是需要作为静态扩展方法注入到实体对象中的,在这个层次的抽象中,我们先忽略掉数据访问层来看问题(
访问层被业务层隐藏了)
。íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi首先我们需要有业务层的接口,在接口的约束之下才可以实现自由的变化。íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi随后我们还需要工厂,工厂通过反射等“万能”方案生产接口的具体实现,封装变化,这里必须封装得很完美,使实现变化的时候代码不需要发动,不然静态方法就完蛋了~
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi最后我们需要一个门面类,门面类提供扩展方法。íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi整个系统的结构如下图所示íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi其中的BusinessFactory
这里为了简化,可以使用反射简单工厂,BusinessFacade
通过扩展方法,将IUser
、IBook
和IBorrow
的方法“注入”到Entity
中。íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi案例展示需求分析在此以一个图书管理系统为例,此系统有三个实体UserInfo
,BookInfo
和BorrowInfo
,功能如下íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi1.
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi用户的增删改,通过ID
获取用户信息íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi2.
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi书籍的增删改,通过ISBN
和作者名获取书本信息íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi3.
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi通过用户获取借阅历史íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi4.
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi通过书本ISBN
获取借阅历史íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi设计实体类设计UserInfo
、BookInfo
和BorrowInfo
三个实体类,实体类的设计依旧按照“贫血”模型设计,不包含任何查询方法,在此不列出代码~
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi设计接口设计三个接口IUser
,IBook
和IBorrow
作为业务层的接口,其代码如下íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiinterface IUseríS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
{íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
voidíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
Insert(UserInfo user);íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
voidíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
Update(UserInfo user);íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
voidíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
Delete(UserInfo user);íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
UserInfoíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
GetByID(int id);íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
}íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
interface IBookíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
{íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
voidíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
Insert(BookInfo book);íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
voidíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
Update(BookInfo book);íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
voidíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
Delete(BookInfo book);íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
BookInfoíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
GetByISBN(string isbn);íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
List<BookInfo> GetByAuthor(stringíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
author);íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
}íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
interface IBorrowíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
{íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
IList<BorrowInfo> GetByUser(UserInfoíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
user, bool includeReturned);íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
IList<BorrowInfo> GetByBook(BookInfoíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
book, bool includeReturned);íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
}
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi创建工厂类工厂类代码如下,在此偷懒就不写反射实例化的代码了,具体可以通过PetShop
等非常多的示例进行学习,总之工厂要求创建出IUser
、IBook
和IBorrow
的具体实现的对象íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰistatic classíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
BusinessFactoryíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
{íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
public static IUseríS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
GetUser()íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
{íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
throw new NotImplementedException();íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
}íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
public static IBookíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
GetBook()íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
{íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
throw new NotImplementedException();íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
}íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
public static IBorrowíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
GetBorrow()íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
{íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
throw new NotImplementedException();íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
}íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
}
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi创建门面类门面类必须是静态的,这样才能进行扩展方法的定义。íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi因为工厂已经封装了变化,在门面类中就大胆地使用吧,不会再有需要多态性支持的地方了~
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi在此以部分代码为例,代码如下íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰistatic classíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
BusinessFacadeíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
{íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
public static void Insert(this UserInfoíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
user)íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
{íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
IUseríS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
bll = BusinessFactory.GetUser();íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
bll.Insert(user);íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
}íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
public static void Insert(this BookInfoíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
book)íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
{íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
IBookíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
bll = BusinessFactory.GetBook();íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
bll.Insert(book);íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
}íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
public static void Update(this UserInfoíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
user)íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
{íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
IUseríS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
bll = BusinessFactory.GetUser();íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
bll.Update(user);íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
}íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
public static void Update(this BookInfoíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
book)íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
{íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
IBookíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
bll = BusinessFactory.GetBook();íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
bll.Update(book);íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
}
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi注意到Insert
和Update
方法的参数中的this
关键字,这就是扩展方法的关键所在。íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi扩展方法依旧属于门面类,只是可以通过实例对象调用íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi编写客户端在此只以简单地示例表示客户端的调用过程,因此创建了一个Console Application
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi假设逻辑是,首先获取ID
为3
的用户信息,然后查询此用户的所有借阅记录,所以有如下代码íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi此时使用门面类,可以看到门面类中所有的方法都有智能提示,扩展方法旁边跟了一个蓝色的箭头。íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi随后通过扩展方法,使用实体对象查询借阅记录,代码如下íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiíS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi可以看到VS
的智能提示可以寻找到扩展方法,因此使用时的体验就和“充血”模型一样啦~
íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi缺点分析首先,对于真正的“充血”模型,可能会有如下的代码íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰiUserInfo user = UserInfo.GetByID(3);íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi但这是无法通过扩展方法实现的,因为扩展方法只能扩展到实例对象,而不能扩展为另一个类的静态方法……íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi其次,“充血”模型在设计时就会给UserInfo
加上GetBorrowHistory
方法,这种对设计的支持在扩展方法的情况下不能很好地实现,一个建议的方法是设计完Business
接口后即刻设计门面类,第一时间给实体对象加上扩展方法,这样至少在单元测试的编写等方面可以有智能提示的支持。íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi最后,不得不提醒的是,扩展方法是静态方法,在下层的封装不完善的情况下一定要慎用!íS
¾
£Hª;www.netcsharp.cnÅÑà×AŰi