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