【领域驱动设计】.NET实践:立足领域

简单的说,软件开发的目的就是通过计算机解决某一领域的实际问题。这样的定义已经将我们的立足点置于领域层面了:我们需要关注的是领域本身,而不是其它的技术细

    简单的说,软件开发的目的就是通过计算机解决某一领域的实际问题。这样的定义已经将我们的立足点置于领域层面了:我们需要关注的是领域本身,而不是其它的技术细节。很多人拿到需求,很喜欢从建立数据模型开始,画出数据模型图、ER图,考虑数据库表的结构,以便开始软件设计。比如,假设现在要设计一套简单的学生成绩管理系统,就管理学生各科的成绩,我们都会毫不犹豫的设计三个数据表:Students(用于保存学生信息)、Courses(用于保存学生所学的科目)以及Scores(用于保存某个学生在某个科目上的成绩)。客观的讲,这种做法不能算错,至少对于交付期很短的项目来说,这样做能够更快地达到目的;但从软件本身的角度看,这样做已经本末倒置了。

    早在两年前,我有简单的写过一篇短文,题目是《软件系统设计一定要从数据库设计开始吗?》,有网友评论说,感觉就从来没有从数据库设计开始过,这样很好,证明起码出发点没有错。在这篇文章里我写的比较简单,可能问题没有得到阐明,现在让我们从另一个角度去考虑问题的本质。正如本文开头所述,我们使用软件来解决领域问题,那么领域问题如何映射到软件系统(软件领域)就成了整个软件设计的关键。根据面向对象理论,我们可以通过对象及其之间的关系来反映领域,以面向对象语言来描述领域,这就使得领域问题能够以最自然的方式“翻译”成软件语言。因此,我们接触得最多的就是这些领域对象,因为只有它们才能合理、准确地在软件范围内将领域问题表述清楚。根据DDD,领域对象可以划分为实体、值对象和服务。既然是对象,必然有其生命周期,就会有“创建”、“使用”、“保存”、“取出”、“撤销”等生命周期状态;工厂和仓储管理了领域对象的生命周期。

    现在再来考虑数据库在整个系统中的位置。在DDD中,仓储用来存放、查找领域对象,并在需要时通过保存的数据重建领域对象。使用仓储最大的好处是,领域模型完全不需要考虑数据保存的细节问题(比如如何保存、保存在哪里),从而使领域模型独立于基础结构层。因此,如果我们的数据保存机制选用数据库的话,仓储就会与数据库打交道,将领域对象保存到数据库里。至此,你会发现,数据库并不是位于软件系统的中心位置,甚至可以说,数据库显得并不那么重要了,数据库只不过是一种保存领域对象的机制。从仓储的角度看,它可以把领域对象保存到数据库,当然还能够保存到文件,更极端一点,假设我们的应用服务器7x24不当机,我们可以直接抛弃数据库,让仓储将领域对象保存在内存里,这将大幅度提高系统的性能。因此,仓储为领域模型提供对象的保存、读取、查询等数据服务,而数据库不过是一种技术选择,它位于DDD 4层中的基础结构层,它的主要任务是保存数据,它根本没办法去描述领域问题。

    对象的关系很丰富:继承、实现、聚合、组合等;数据库关系就相对简单了:1:n,n:m。如何将对象关系保存到数据库中,我们可以借助ORM来解决这样的问题,比如NHibernate。同样,ORM位于基础结构层,它不懂领域。在整个软件系统中,数据持久化那是ORM的事情,是用一个数据表保存领域对象,还是使用主从表外键关联的方式保存,都与我们无关。

       好了,到这里我想你也应该可以慢慢地走出数据库的阴影,回归到领域模型本身上。在设计领域模型时,我们又容易踏入另一个误区:认为领域模型就是简单的POCO(Plain Old CLR Objects)及其之间的关系集合。DDD提倡“富领域模型”,这就意味着,应该尽量将业务逻辑置于领域对象里,而对于那些理论上不属于任何领域对象的业务逻辑,应将其置于服务中。这样做的好处是很明显的:因为领域对象是现实世界的面向对象表述,它不但具有属性,而且还应该有自己的行为,它的行为甚至还能触发一些其它的事情。那么什么时候使用POCO?POCO用于构建数据传输对象(DTO),以便能够集中表示数据并让数据在软件系统的各个层间自由传递,它还具有隐藏领域业务逻辑的功能。关于DTO,我会在后续的章节中讨论。

    现在来简单的看一下文章开始部分的那个例子,假如成绩管理系统需要计算每个学生的总分,我想我们的“学生”类大致可以有类似如下的定义:

view plaincopy to clipboardprint?
  1. public class Student : IEntity   
  2. {   
  3.     public Student()   
  4.     {   
  5.         this.DayOfBirth = DateTime.Now;   
  6.     }   
  7.   
  8.     /// <summary>   
  9.     /// 读取或设置学生的姓氏。   
  10.     /// </summary>   
  11.     public string LastName { getset; }   
  12.     /// <summary>   
  13.     /// 读取或设置学生的名字。   
  14.     /// </summary>   
  15.     public string FirstName { getset; }   
  16.     /// <summary>   
  17.     /// 读取或设置学生的出生日期。   
  18.     /// </summary>   
  19.     public DateTime DayOfBirth { getset; }   
  20.     /// <summary>   
  21.     /// 读取或设置学生的成绩列表。   
  22.     /// </summary>   
  23.     public IList<Mark> Marks { getset; }   
  24.   
  25.     /// <summary>   
  26.     /// 获得学生的年龄。   
  27.     /// </summary>   
  28.     public int Age   
  29.     {   
  30.         get { return DateTime.Now.Year - this.DayOfBirth.Year; }   
  31.     }   
  32.   
  33.     /// <summary>   
  34.     /// 计算学生的总成绩。   
  35.     /// </summary>   
  36.     /// <returns>总成绩。</returns>   
  37.     public float GetTotalScore()   
  38.     {   
  39.         float total = 0;   
  40.         foreach (Mark mark in this.Marks)   
  41.         {   
  42.             total += mark.CourseMark;   
  43.         }   
  44.         return total;   
  45.     }   
  46.   
  47.     public override string ToString()   
  48.     {   
  49.         return string.Format("{0}{1}",   
  50.             this.LastName, this.FirstName);   
  51.     }  
  52.  
  53.     #region IEntity Members   
  54.     /// <summary>   
  55.     /// 读取或设置学生的编号。   
  56.     /// </summary>   
  57.     public Guid Id { getset; }  
  58.     #endregion   
  59. }   

    本文主要阐述了两个观点:1、软件设计必须立足领域,以领域为关注核心,通过使用面向对象等手段尽可能合理地将领域问题映射到软件系统(领域模型)上;2、领域模型应该是“富领域模型”,否则无法完整、自然地表述领域问题。DDD实践有着很长的路要走,在实践的过程中,我们碰到的问题其实多数没有唯一准确的答案,只能说哪种答案更为合理。作为一名软件人员,我也深刻能够体会到大多数软件人员更喜欢将精力放在解决技术问题上,这样会更容易地得到他人对自己的赏识,然而,当我们真正需要设计一个软件系统时,我们不得不更多地去学习领域知识、关心领域问题,而这些内容大多是跟技术不搭界的。Eric Evans有句话说的很有意思:"Developers get interested in the domain when the domain is technical."(Domain-Driven Design: Discussion by SVPG),意思是,其实开发人员也关注领域,但是是技术领域。立足领域,DDD之道。