最近在学习领域模型,一直纠结在领域层跟数据库层的划分,网上也查了不少资料,趁这段时间有空,好好总结下!

 

如何进行逻辑划分:

       在软件架构中,最常见的一种架构模式就是层式架构模式,即把一个系统按逻辑上的功能拆分成多个层,层与层之间保持单向依赖关系,每层只依赖于其直接下层,以保证每层的良好封装性和独立性。而层式架构模式最常用的就是:展示层-服务层-数据访问层,应用领域驱动设计(充血模型),服务层还应被进一步拆分为:应用服务层和领域层。

 

      综上所述,每个模块(子系统)又可以拆分成:展示层-应用服务层-领域层-数据访问层四个逻辑层。

 

      重要原则:每层只依赖于其直接下层,层与层之间不能形成循环依赖关系。

 

问题所在:

      领域层跟数据库层关系非常密切,所以我们放在一齐进行论述。两者的关系是:数据访问层依赖于领域层。

 

       先来看看领域层,领域层包含了该模块的领域对象和这些领域对象对应的仓库接口(Repository——对应J2EE设计模式的DAO,为了让设计和开发人员在进行建模的阶段尽量脱离具体技术去进行建模,我们建议使用Repository的模型术语取代DAO这个技术化的出于设计模式的术语),注意,这里包含的是仓库的“接口”,而非实现。对于这样的划分,一直存在着争议,最大的争议点在于:领域层的职责是负责领域业务逻辑处理和封装,不应该把数据访问的相关职责也纳入领域层。下面我们对这种设计进行分析和论述,并提供案例作为理据去论证这种设计的原因。

 

      在现实世界中,完成一个业务,并不需要有所谓“持久化”的操作,但这种模型一旦反映到软件中,“持久化”就变成了必不可少的步骤,我们从两个方面去进行分析:

 

    从领域层本身来看,由于领域对象本身包含了绝大部分的业务逻辑,在一些复杂的业务逻辑中,我们有时候出于性能等因素的考虑,是需要在领域对象被加载到内存后,在其业务逻辑中还需要通过额外的查询去查找完成某个业务所需的数据,而这些数据通常并不是领域对象自身的数据,而可能是从属这个领域对象的其他领域对象的数据,下面举一个例子来说明:

 

 

      上图是案例系统的出版物管理模块的其中一个主要的领域对象:Pulication——刊物,我们看到,Publication有两个从属的领域对象:PriceItem(历史价格明细)和IssueSchedule(历史出版发行计划),PriceItem保存的是对应Publication在不同时间段的不同销售区域的指定币种的销售价格,IssueSchedule保存的是对于Publication在不同时间段的出版发行计划。现在假设有一个订单业务,需要在某个时间段订阅某Publication,那么,为了计算订单的金额,则必须通过该Publication获取两个关键数据:该Publication在指定区域的指定币种的当前销售价格,以及该Publication在这段时间的发行期数。而这两个数据,均来源于Publication本身,也就意味着,Publication领域对象具有两个职责:获取当前区域指定币种销售价格——getCurrentPriceForCurrency和统计指定时间段的出版期数——countIssues,我们有两种方案去实现这个两个方法:

 

      一、通过ORM工具(Hibernate)对Publication的两个聚合对象集合PriceItem、IssueSchedule进行延迟加载设置,当调用该方法时,就可以不显式的调用Repository,而由ORM自动实现所有从属对象的加载。

 

      二、在两个方法的方法体内,显式的调用Repository接口提供的专有查询方法来获取相应数据,该专有查询方法并不把所有从属对象全部加载到内存,而是通过具体的持久化实现的查询语言(SQL、HQL、XQuery等),把符合条件的数据查询出来,而且,该专有查询方法并不带有任何与具体持久化实现耦合的信息,它只提供抽象,具体实现由数据访问层的领域对象仓库实现类提供。

 

      我们来对比一下两者,前者有两个明显的缺陷:1)从方法的职责上来说,方法只返回符合条件的结果,而这种实现会把所有从属数据(并且包括所有字段)都加载到内存,然后进行筛选,这种做法对内存的损耗是可以非常可观的(试想如果历史数据已经积累了很多年,数据量很大的情况)。2)这种设计(实现)方法是依赖于具体技术的,这就意味着,如果具体技术发生改变,会直接影响到Publication的模型,甚至产生更大的连锁效应。而第二种实现方案恰恰弥补了上述两种严重的缺陷。有人可能会认为,第二种方案的最大缺点是让领域对象与Repository形成了一种双向依赖关系而导致两者无法分开,但我们从本质上分析,第一种方案中,其实领域对象Publication依然无法逃脱其业务逻辑需要查询其从属领域对象这个潜在要求,只不过,第一种方案利用了ORM工具的技术手段,让代码中不需要建立这种显式的依赖而已。所以,从功能上来讲,无论何种实现方案,只要业务上有这样的要求(如上例),领域对象是无法与Repository完全脱离关系的。

 

      综上所述,我们从领域层本身出发,得出的结论是:领域对象与领域对象仓库之间存在着天生的互相依赖关系,这种关系在业务简单的时候可能体现不出来,但随着业务的进展,这种潜在的依赖关系很大机会会出现。因此,无论开始的时候业务是否足够简单,以至于领域对象的业务逻辑并不需要有额外的查询,出于对模型的统一的考虑,还是应该在开始设计的时候,把领域对象仓库接口划入领域层(即使它从职责划分原则上来看并不完全合理,但这世界上通常没有完美的解决方案,只有最适合的解决方案)。

 

      从应用服务层的角度看,对于领域层,应用服务层是其客户程序,因为应用服务层依赖于领域层提供领域对象完成相应服务。正如前面所述,在软件世界中,要完成一个业务,“持久化”几乎必不可少,那么,对于应用服务层来说,单独的使用领域对象或领域对象仓库并没有任何的意义,因为应用服务层如果要使用领域对象,则必须通过领域对象仓库对其进行加载,如果是创建新的领域对象,也必须(绝大部分情况)通过领域对象仓库实现领域对象的持久化。同时,应用服务层使用领域对象仓库的唯一目的就是实现对领域对象的增删改查操作。

      综上所述,我们从应用服务层的角度看,对于应用服务层来说,领域对象和领域对象仓库本身就是一体化的东西,无法割裂。

     经过上述的详细分析,我们已经有充足的理据支持领域对象与领域对象仓库被统一划入领域层的设计。

 

      我们再来分析数据访问层,前面已经说过,数据访问层是依赖于领域层的,它实际上是为应用服务层提供领域层领域对象仓库的具体实现,通过这样的设计,领域层虽然把数据访问的抽象纳入到其职责范围之内,但依然与具体的数据访问实现细节解藕,只要数据访问的抽象不发生变化,持久化实现发生变化只会影响数据访问层,对其他层是完全透明的。