本章大部分内容摘自:《领域驱动设计:软件核心复杂性应对之道》一书中的第四章,分离领域,纯属原创。如有错误请指正,相互学习。
在软件中,专门用于解决问题的那部分通常之占整个软件的系统的很小一部分,这与其重要性远远不成比例。要想实现最佳的设计构思,就得去研究模型中的元素并它们视为一个系统
模式:LAYERED ARCHITECTURE (分层结构)
在面向对象的程序中,常常会在业务对象中直接写入用户界面、数据库访问等支持代码。而一些额外的业务逻辑则会被嵌入到用户界面组件和数据库脚本的行为中。这么做是为了以最简单的方式在短期内完成开发工作。
如果与领域有关的代码大量分散在大量的其他代码之中,那么查看和分析领域代码就会变得相当困难。对用户界面的简单修改实际上很可能会改变业务逻辑,而要想调整业务规则也很可能需要对用户界面代码、数据库操作代码或者其他的程序元素进行仔细的筛查。这样就不太可能实现一致的、模型驱动的对象了,同时也会给自动化测试带来困难。程序中每一个活动都有其自身的逻辑和使用的技术,因此程序本身必须简单明了,否则就会让人无法理解。
要想创建能够处理复杂任务的程序,需要把不同的关注点分开考虑,是设计中的每个部分都得到单独的关注,在分离的同事,也需要维持系统内部复杂的交互关系。
分割软件系统普遍采用LAYERED ARCHITECTURE(分层架构),有些层已成标准。基本原则是层中的任何元素都仅依赖于本层的其他元素或其下层的元素。向上通信必须通过间接的传递机制进行。分层的价值在于每一层只代表程序中的某一特定方面,这种限制使每个方面的设计都更具内聚性,更容易解释。
大多数成功的架构使用都包括下面四个概念层的某个版本:
- 用户界面层(或表示层):负责用户显示信息和解释用户指令,这里指的用户是另一个计算机系统,不一定是使用用户界面的人。
- 应用层:定义软件要完成的任务,并指挥表达领域概念的对象来解决问题。这一层所负责的工作对业务来说意义重大,也是与其他系统的应用层进行交互的必要渠道。应用层要尽量简单,不包含业务规则或者知识,而只为下一层中的领域对象协调任务,分配工作,使他们互相协作。它没有反应业务情况的状态,但是却可以具有另外一种状态,为用户或程序显示某个任务的进度。
- 领域层(或模型层):负责表达业务概念,业务状态信息以及业务规则。尽管保存业务状态的技术细节是有基础设施层实现的,但是反应业务情况的状态是有本层控制并且使用的,领域层是业务软件的核心。
- 基础设施层:为上面各层提供通用的技术能力:为应用层传递消息,为领域层提供持久化机制,为用户界面层回执屏幕组件,等等。基础设施层还能够通过架构框架来支持四个层次间的交互模式。
给复杂的应用程序划分层次,在每一层内分别进行设计,使其具有内聚性并且只依赖于它的下层。采用标准的架构模式,只与上层进行松散连接。将所有与领域相关的代码放在一个层中,并把它与用户界面层、应用层以及基础设施层的代码分开。领域对象应该将重点放在如何表达领域模型上,而不需要考虑自己的显示和存储问题,也无需管理应用任务等内容。这使得模型的含义足够丰富,结构足够清晰。
各层之间可以生成一个应用程序,也可以每层部署为一个应用,在分布式系统中每层可以灵活的放在不同的服务器或者客户端中。
将各层关联起来
程序需要划分层次,但是各层之间也需要互相连接,在连接各层的同事需要不影响分离带来的好处。各层之间是松散连接的,层与层之间的以来关系只能是单向的,上层可以直接使用或操作下层元素。方法是保持对下层元素的引用,采用常规的交互手段调用下层元素的公共接口。如果下层需要与上层元素通信,则需要采用另一种通信机制,如使用架构模式来链接上下层,比如回调模式或观察者模式。
链接用户界面层与应用层有很多种方式,只要连接方式能够维持领域层的独立性,保证设计领域对象时不需要同时考虑可能与其交互的界面,那么连接方式就可用。
一般基础设施层只提供服务,如:发送邮件的消息发送功能,持久化操作功能、导出excel功能等,但有时有些技术组件被设计成直接支持其它层的基本功能,比如:为所有领域对象提供抽象基类等.
架构框架
整合了大量基础设施需求的框架常会需要其他层以某种特定的方式实现,比如以框架类的子类形式或者带有结构化的方法签名。最好的框架既能解决复杂技术问题,也能让领域开发人员集中精力去表达模型而不考虑其他问题。然而使用框架很容易为项目制造障碍,要么设定了太多假设,减少领域设计的可选范围;要么是需要实现太多的东西,影响开发进度。
项目中一般都需要某种形式的架构框架(尽管有时项目团队选择了不太合适的框架)。当使用框架时,应该明确其使用目的,建立一种可以表达领域模型的实现并且用它来解决重要问题。项目团队必须想方设法让框架满足这些需求,即使这意味着要抛弃框架中的一些功能。
模型属于领域层
领域模型是一系列概念的几核。“领域层”则是领域模型以及所有与其直接相关的设计元素的表现,它由业务逻辑的设计和实现组成。在MODEL-DRIVEN DESIGN中,领域层的软件构造反应出了模型概念。
模式:THE SMART UI "ANTI-PATTERN"
如果一个经验并不丰富的项目团队要完成一个简单的项目,要使用MODEL-DRIVEN DESIGN 以及 LAYERED ARCHITECTURE,那么项目组将会经历艰难的学习过程。掌握新技术、学习对象建模、对基础设施及各层的管理工作使原本简单的任务却需要花费很长的时间来完成。简单项目开发周期较短、期望值也不高,所以可能任务完成之前项目木就会被取消。即使项目有时间,没有专家帮助,团队成员也不太可能掌握这些技术。最后假如克服了这些困难,恐怕也只会开发出一套简单的系统,因为项目本来就不需要丰富的功能。
因此,当情况需要时:在用户界面中实现所有业务逻辑。将应用程序分成小的功能模块,分别将它们实现成用户界面,并在其中嵌入业务规则。用关系数据库作为共享的数据存储库。使用自动化程度最高的用户界面创建工具和可用的可视化编程工具。
这也是一个有争议的观点,但是SMART UI有着自身的优势,效率高、学习成本低等,但是一旦使用了SMART UI如果补充些代码将无法在使用领域驱动设计。
项目团队也经常犯一个错误就是采用了一种复杂的设计方法,却无法保证项目从头到尾始终使用它。另一种常见的也是代价高昂的错误则是为项目构建一种复杂的基础设施,并使用顶级的工具,而这样的项目根本不需要它们。
将领域设计与其他部分混在一起会产生灾难性的后果,所以不要随意使用多种模式。
Example:
项目分层结构
将应用程序分层,并对指定层隐藏类的可见度,使用maven的多模块项目是个不错的选择,由于接触尚少,尚不知还有没有其他的办法,如有请指点相互学习,下面参考下koala项目的项目结构及编写一个小demo。关于maven的知识我就不多说了,请大家自行搜索吧。首先看下目录结构:
demo:为父项目,进行一些公共配置
demo-facade: 外观层接口
demo-facade-impl:外观层实现类,实现demo-facade层的接口
demo-application:应用层
demo-infra:基础设施层
demo-conf:基础设施层,主要是一些配置文件信息
demo-web:用户界面层
demo-core:领域层
由于koala项目分层较多,其中外观层在一些相对简单一些的项目中可以去掉,因为外观层主要是封装各个子系统的细节来提供统一的接口对外发布,可以绕过外观层直接让用户界面层访问应用层。
根据分层基本原则,本层依赖或操作本层及下层的元素,依赖是单向性的,koala项目的依赖方向如下:
demo-web项目为界面层,主要显示用户信息与解释用户指令,即封装好参数然后传递给外观层。引用其他项目的配置信息为:
<dependency>
<groupId>com.yunzaipiao</groupId>
<artifactId>demo-facade</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>com.yunzaipiao</groupId>
<artifactId>demo-conf</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.yunzaipiao</groupId>
<artifactId>demo-infra</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
demo-facade与demo-facade-impl项目为外观层,这一层为应用层上面的一层,对应用层进行了进一步的抽象,如果项目不是太复杂,没有太多的子系统,可以没有这一层。demo-facade为接口定义项目,一般不需要引用其他的项目,demo-facade-impl是外观层的接口实现项目,需要引用下层项目,不过上面也说过,同层元素也是可以相互引用的,所以这里也可以引用demo-facade项目,因为他们在同一层,只不过不是一个项目而已,下面为demo-facade-impl引用其他两个项目的配置信息:
<dependency>
<groupId>com.yunzaipiao</groupId>
<artifactId>demo-application</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>com.yunzaipiao</groupId>
<artifactId>demo-facade</artifactId>
<version>${project.parent.version}</version>
</dependency>
demo-application 为应用层,应用层定义要完成的任务和指挥领域层解决问题,不存储业务规则,应用层可以引用下层项目及基础设施层,配置信息如下:
<dependency>
<groupId>com.yunzaipiao</groupId>
<artifactId>demo-core</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>com.yunzaipiao</groupId>
<artifactId>demo-infra</artifactId>
<version>${project.parent.version}</version>
</dependency>
demo-core 为领域层项目,处理业务规则,可以引用基础设施层,主要通过基础设施层来进行持久化操作。由于一个基础设施层可以有多个项目组成,这里把持久化操作的相关内容都定义在了其他的项目中,引用的配置如下:
<dependency>
<groupId>org.dayatang.dddlib</groupId>
<artifactId>dddlib-query-channel</artifactId>
<exclusions>
<exclusion>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
</exclusion>
</exclusions>
</dependency>
demo-infra 是基础设施层项目,由于基础设施层为项目的最层,所以一般不会对其它层进行直接操作,这里也就没有配置引用信息。
编写应用层及领域层实例
现在模仿一个非常简单的用户注册的例子,其中领域模型的getter setter方法都不再显示了,还有一些其他的在此不重要的代码都不写了。在用户注册时,登录用户名与昵称是不能重复的,所以在注册的时候需要验证用户的登录用户名与昵称是否已经存在,验证时的查询数据库等代码也没有实现,请简单看下吧。
首先是领域模型,UserModel的代码
package com.yunzaipiao.demo.core.domain;
import org.openkoala.koala.commons.domain.KoalaAbstractEntity;
import com.yunzaipiao.demo.core.exception.LoginNameExistsException;
import com.yunzaipiao.demo.core.exception.NickNameExistsException;
/**
* 用户 Model
* @created 2016年3月23日 下午9:13:35
*/
public class UserModel extends KoalaAbstractEntity {
/**
* 登录用户名
*/
private String loginName;
/**
* 密码
*/
private String password;
/**
* 昵称
*/
private String nickName;
/**
* 真实姓名
*/
private String realName;
/**
* 判断登录用户名是否存在
*
* @created 2016年3月23日 下午9:12:04
*/
private void isExistsLoginName() {
if (true) {
throw new LoginNameExistsException("loginName is exists!");
}
}
/**
* 判断昵称是否存在
*
* @auth 周志君
* @created 2016年3月23日 下午9:12:25
*/
private void isExistsNickName() {
if (true) {
throw new NickNameExistsException("nickName is exists!");
}
}
public UserModel(String loginName, String password, String nickName,
String realName) {
isExistsLoginName();
isExistsNickName();
this.loginName = loginName;
this.password = password;
this.nickName = nickName;
this.realName = realName;
}
// getter setter ....
}
在领域类中包含四个成员变量,以及三个方法,分别是:
isExistsNickName:这个方法用来判断用户的昵称是否已存在,如果已存在则抛出异常
isExistsLoginName:这个方法用来判断用户登录用户名是否已存在,如果已存在则抛出异常
UserModel(String loginName, String password, String nickName, String realName):构造方法,该构造方法,首先调用上述的两个方法判断用户昵称及用户登录名是否已存在,如果已存在则继续向上抛出异常有应用层进行处理。
下面看看应用层的代码,首先是接口只定义一个注册的接口,如下;
package com.yunzaipiao.demo.application;
public interface UserApplication {
/**
* 注册接口
* @param loginName
* @param password
* @param nickName
* @param realName
* @created 2016年3月23日 下午9:24:20
*/
public void reg(String loginName, String password, String nickName, String realName);
}
下面是接口实现类,实现类的方法中,只需要new一个UserModel,然后调用UserModel的save方法即可,save方法是UserModel继承的KoalaAbstractEntity中的方法,可以将领域类持久化到数据库中,代码如下:
package com.yunzaipiao.demo.application.impl;
import com.yunzaipiao.demo.application.UserApplication;
import com.yunzaipiao.demo.core.domain.UserModel;
public class UserApplicationImpl implements UserApplication {
@Override
public void reg(String loginName, String password, String nickName,
String realName) {
UserModel userModel = new UserModel(loginName, password, nickName, realName);
userModel.save();
}
}
外观类与界面类就不写实例了,因为都是简单的将参数封装一下传递过来就可以了,一般没有什么业务可以处理的。这里的接口类使用的传递多个参数的方式,也可以将参数封装到一个DTO中,直接传递一个类过来,不顾这里为了方便也没有在生成其他的类。
不过有这里简单的代码可以看出,应用层只是定义要完成的任务如用户注册。而具体如何注册,有哪些业务规则,如登录用户名不能重复,昵称不能重复等,则有领域类来进行处理。应用层定义任务,然后指挥者领域层进行完成任务,基础设施层协助应用层或领域层。界面层则是处理用户下达的指令并解释成应用层可以理解的参数。
本章大部分内容摘自:《领域驱动设计:软件核心复杂性应对之道》一书中的第四章,分离领域,纯属原创。如有错误请指正,相互学习。