2015-11-11 17:37:41 u010870518 阅读数 17538

背景

使用DDD开发大概也有五个月的时间了,由于当时公司导师的推荐,第一次接触DDD领域驱动到现在彻底迷恋这种开发的模式,为其思想的奥妙所折服,一直以来,总想花一点时间来总结一下,正直光棍节(天猫狂欢购物节)当天,“静下心来”(PS:没有人民币)总结一下。

说起DDD不得不说一篇文章:http://www.cnblogs.com/netfocus/archive/2011/10/10/2204949.html

第一次接触,就是这篇文章,当时看起来晦涩难懂,后来慢慢的读起来,每一次都有精神的提升,网上也有很多关于DDD的介绍,这里将不会在介绍概念什么的,下边主要是自己工作过程中的一些总结。

如何快速入门

第一次接触DDD的时候,概念高深莫测,奥秘深不可见,大有不知所云的趋势,后来导师的引导,让直接从项目中直接入手,遂逐步揭开其一层层雾纱,如图1:

这里写图片描述

DDD一般的分层结构和调用顺序如上所述,infrastructure是基础设施层,domain是领域层,application是应用层,facade和facade-impl是门面层(前者是门面接口层,后者是门面实现层),webapp是用户接口层(采用web形式)。

下边是一种项目的分层图,采用的Maven管理代码。图2:

这里写图片描述

分层的介绍

1、web:首先包含网站前端,如果使用SpringMVC的话,还需要其Controler,

这里写图片描述

该Controler层主要是用于接收HTTP请求和返回客户端,一般不进行逻辑上的判断,其代表了用户可以进行的操作,一般不涉及领域驱动的思想,示例代码:

@ResponseBody
    @RequestMapping(value = "/create", method = RequestMethod.POST)
    public String create(AirlineWhiteListDTO airlineWhiteListDTO) {
        MDC.put(ConstString.TRACE_ID, LogUtil.getTraceId(ConstAirlineWhiteList.CREATE));
        Response response = airlineWhiteListFacade.create(airlineWhiteListDTO);
        return super.handleResponse(response);
    }

第一行使用MDC用于记录(打log)http请求的顺序和调用的方法,第二行调用“门面层”facede,获得Response返回对象,第三行用于判断是返回界面还是返回data等操作,即时SpringMVC的ModalAndView;

2、facade和facade-impl一个是门面层接口一个是门面层接口的实现类,示例代码:

@Autowired
IAirlineWhiteListApplication airlineWhiteListApplication;

public Response create(AirlineWhiteListDTO airlineWhiteListDTO) {
        Response response = new Response();
        try {
            AirlineWhiteList airlineWhiteList = AirlineWhiteListAssembler.toEntity(airlineWhiteListDTO);
            airlineWhiteListApplication.create(airlineWhiteList);
        } catch (Exception e) {
            response = new Response(e);
        }
        return response;
    }

可以看出这是在web的controller中访问的方法,主要进行数据的组装(实体对象和数据传输对象的转换),以及返回对象Response的转换。

之所以成为门面层,我们暂可认为是我们用户可以看到的整个系统的东西,例如上述中的Response就是返回给用户看的东西。

3、Application层,上述的facade调用到了application中的方法,

@Inject
private AirlineWhiteList airlineWhiteList;

public boolean create(AirlineWhiteList airlineWhiteList) {
        return this.airlineWhiteList.add(airlineWhiteList);
    }

这一层主要是操作实体对象的,是于数据更近一层的操作,主要定义了用户所拥有的方法和属性,操作实体对象层,将实体对象所有的方法展示出来供用户使用;

4、domain层,数据实体层,相比MVC中的modal简单的只是数据库的映射,这种“毫无灵魂”的对象,领域模型中不但有一个实体对象的属性还有其方法,(我们可以在实际使用的时候使用继承DTO的方式),在这一层中定义了实体对象操作数据库的方法;

这里我们的实体对象不仅是拥有属性还具有方法的,就像一个人一样,我们不但拥有做人的基本特征(两手、两脚等),我们还有属于自己的技能(方法),这样的话才是一个拥有灵魂的东西;

5、infra这一层包含了访问数据库的方法、数据仓储(sql、nosql、api)和一些工具类,service等;

由于是domain层调用该层的,实体对象的操作固然包含CRUD,既是我们需要进行对数据库的操作,当我们只有一个数据源的时候,很简单,但是后期项目中数据源可能会增加,可能会添加缓存等,这样的话使用原来的MVC模式的话,我们可能需要修改很多,如下图:

这里写图片描述

但是使用DDD的话,由于我们的Domain调用的是infra 数据仓库Repository接口,Repository中定义了访问数据源的方法,这样的话,当我门新增数据源的时候,我们的Domain层以上都无需修改,只需进行infra层的修改即可。
这里写图片描述

上图中有2个数据源,一个是sql一个是nosql,在方框中的Repository即调用的是nosql和sql中的方法,这样的话Domain直接调用Repository即可。

领域驱动的示例:

如果想快速的领回DDD的奥妙,这里有一个案例,一个很不错的开源系统,使用的正是DDD思想,可以把代码下载下来仔细研究,其中的思想是很不错、很不错的,地址如下:http://www.openkoala.org/

总结:

类比MVC“哑铃式”的分层结构中,Model和View代码少,Controller代码臃肿的布局格式,DDD拥有更多的分层,各层之间各司其职,协调工作,一步步调用,井然有序,对于后期的维护,只要是熟悉DDD的开发人员都可以很好的维护。

2013-01-29 16:43:58 iteye_11133 阅读数 76

最近花了一点时间了解Sculptor-一款面向领域模型开发利器,发现其设计理念和功能实现相当不错。以面向模型驱动开发的方式,将DDD的概念和模式运用于DSLDomain Specific Language)之中,并为其预置了HibernateSpring代码框架实现,并且内置很多扩展性特点,完全区别于以往普通的代码生成器,使得开发者更加关注与需求功能实现,而摆脱技术框架的束缚,大大提高了开发效率。

建议对领域模型驱动开发感兴趣的同学都了解一下,官网地址如下:
http://fornax.itemis.de/confluence/display/fornax/Sculptor+(CSC)

本文抛砖引玉摘取官网部分wiki内容进行介绍。

概述

Sculptor是一个简单而强大的代码生成平台,提供了基于MDSDModel Driven Software Development)的快捷通道。当你使用Sculptor时,你可以精力集中于面对业务领域建模,而替代关注技术实现细节的传统方式。你可以将来自于DDDDomain-Driven Design)的概念抽象描述到DSLDomain Specific Language)原始文本文件中,Sculptor 将使用XTextXpand去解析该DSL文件,并生成高质量可用的Java代码和配置文件,这些生成的代码都基于著名的业界框架,如SrpingHibernateJava EE

 

 

 

上图描述了开发人员如何使用DSL描述应用,使用Maven生成代码和配置文件。生成的代码和手工编写的代码很好的进行了分离。手工编写的代码,如Junit测试用例、业务逻辑等将添加到子类或者其他定义良好的位置中。

在开发过程中的DSL和代码生成驱动并非昙花一现,而是在整个过程迭代反复,可以和TDDTest Driven Development)结合并随设计演讲,正如在Test Driven Development with Sculptor索引中的解释。

Sculptor亮点介绍:

ü  运用DDD的概念在DSL原始文件中直接描述编程语言,如:ModuleServiceEntityValue ObjectRepository

ü  HibernateSpring提供了最佳实践。目标环境是一个真正可用的商业系统。它很容易实现具备CURD的服务层,但这样的设计假定还需比简单的CURD设计(如灵活性、业务逻辑等);

ü  Sculptor的设计和代码生成机制比目前流行的生成工具(如Hibernate synchronizeHibernate reveng)更加完善,可以即开即用。或许最大的优势便是可以通过一个独立的模型生成一套完整的应用程序,而不是一个需要全面进行设计的代码片段;

ü  快速上手。最初的MDSD起步可能会很臃肿,但Sculptor减少了很多比必要的枝节;

ü  Sculptor并不是万能的工具,但足以适合大多数系统开发,而定制化的内容迟早也将要面对。Sculptor的设计和文档还在不断演进,最终的结果将会应你的需求更加易用。

 

快速起步

这里将使用一个例子的实践说明Sculptor。这个例子完整的介绍还没有完全在用户手册中描述,但可以在SculptorWiki中找到。

这个例子是一个电影和书籍的library管理系统。这个系统的核心业务模型如图所示:


 

Maven Archetype

通过Maven运行一些简单的命令就可以为应用程序生成Eclipse工程,Sculptor提供了这样的Maven Archetype去面对这些工作。

Domain Specific Language(DSL)

一个基于Sculptor的应用被描述于一个DSL原始文件中,DSL文件是一个最为原始文本文件代码,可以进行查找、复制、粘帖和合并等特性,SculptorDSL提供了Eclipse编辑器,可以支持高亮显示错误、代码检查和概览。

在刚开始Library例子中,DSL对模型的描述如下:

Application Library {

basePackage = org.library

 

Module movie {

Service LibraryService {

findLibraryByName delegates to LibraryRepository.findLibraryByName;

saveLibrary delegates to LibraryRepository.save;

findMovieByName delegates to MovieRepository.findByName;

}

Entity Library {

String name key

reference Set<@Movie> movies

Repository LibraryRepository {

save;

@Library findLibraryByName(String name)

throws LibraryNotFoundException;

protected findByCriteria;

}

}

Entity Movie {

String title not changeable

String urlIMDB key

Integer playLength

Repository MovieRepository {

List<@Movie> findByName(Long libraryId, String name)

delegates to FindMovieByNameAccess;

}

}

}

}

 

这里DSL定义了两个模型和一个服务逻辑。它可以定义类似的模型,包括模型的属性和应用。

DSL的核心概念源自于DDD,如果你还没有对DDD没有认识,那么可以下载《DomainDrivenDesignQuickly》进行了解。

 

代码生成

Sculptor的代码生成过程定义为Maven构建工程的一部分,如使用命令mvn install将会执行代码生成。当进行部署时使用mvn generate-sources,将只会生成代码,而没有编译、测试和打包过程。

代码生成后被手动编写完善的代码将会视图成为完整的构件,而其他生成的构件将会每次都重新生成和覆盖。代码生成的保存分离如下图所示:


 

每次都会重新生成的代码不应加入版本控制。这里有两种类型的构件位于每个文件系统之中。sourceresources 目录结构如下图所示。这里同样展示了包名和生成的class类。这些名称都可以很容易的修改。


 

与其他代码生成工具相比,Sculptor一个很强大的亮点在于贯穿于整个完整的应用,而不仅仅是还需要很困难的进行整体设计填充的一些代码片段。

Sculptor通过简单的设计,减少了大量50%相似度一致的需要手工编写的代码。

领域模型层

Sculptor的语境中,领域模型Domain Object是一些通用的对象,如EntityValue ObjectBasic Type

实体拥有标识和状态,并且可以在其生命周期中改变。对于Value Objects 属性的值是感兴趣的,而不是其对象本身,Value Objects 一般来说都是不可变的。Basic Type用于定义基础类型,如钞票。Basic Type 是属于Value Object,并且和Domain Object仓库与同一张表中,并被Domain Object引用,它可以通过JPA embedded内嵌进行协作。

Domain Object可以被实现为原始的POJOs或者EJB3的实体。它们可以拥有相同的属性和引用其他Domain ObjectDomain Object当然可以包含行为,否则无法成为一个胖Domain Object。尽管如此,行为逻辑一般通过手工编写,而不会定义到DSL中。

这里很有可能定义一个不会被持久化于数据库的Value Object。这里的例子展示用于服务操作请求的参数和返回值。这里也很有可能被定义为一个原始的Data Transfer Object,从而运用于外部的服务,例如web service

以下是Library例子中定义在DSL文件中的一部分Domain Object

Entity Person {

String ssn key length="20"

String country key length="2"

Integer age

reference @PersonName name

}

BasicType PersonName {

String first

String last

}

ValueObject MediaCharacter {

String name not changeable

reference Set<@Person> playedBy

reference Set<@Media> existsInMedia opposite characters

}

对于Domain ObjectSculptor将生成如下内容:

ü  一部分Domain Object的数据;

ü  Domain ObjectJPAHibernate对属性和关联的注解;

ü  Data Transfer Object JAXB注解;

ü  构造函数和访问方法;

ü  Hibernate验证框架注解;

ü  hashCode toString方法;

ü  findByCondition使用的属性元数据;

ü  数据库定义脚本;

ü  可视化类图;

ü  可以点击预览的HTML格式化文档;

 

生成代码基类与需手工编写实现逻辑的代码子类分离。在图Figure 3, "Separation of generated and hand written". 在子类中增加方法,实现Domain Object的行为。这些子类只会被生成一次,之后便不会被代码生成器重写。当然,你也可以删除掉它们后,重新由代码生成器再次生成。对于Hibernate而言,equalshashCode是必须的,Sculptor很注重实现细节,这个例子展示了最佳实践。唯一需要做的就是在Domain Object属性中标识natural key,否则Sculptor将会自动生成UUIDSculptor DSL基于贯穿配置的理念,一个例子说明Entities生成是将是默认可以进行审计,这意味着这些对象被保存时,一个拦截器将会自动的更新信息记录是谁、在什么时间新增或修改了对象的属性,这些功能将会默认的增加到可审计的Domain Object中,当然你可以将此默认功能关闭,对于Value Objects默认不会有审计功能。

服务层

Services在领域模型Domain Model中扮演服务层的职责,它为客户端提供了一组定义良好可用操作。

Services 默认被注解为@ServiceSpring框架实现接口和实现类,当然也支持EJB3无状态的session bean

对于事务的绑定位于服务层,JPA/Hibernatesession划分和错误处理以SpringAOP实现

。在DSL的服务层定义一个操作看上去跟原始的Java方法一样,方法中有返回类型、参数和异常抛出定义。如:

Service LibraryService {

inject LibraryRepository

inject MediaRepository

@Library findLibraryByName(String name)

throws LibraryNotFoundException;

 

saveLibrary delegates to LibraryRepository.save;

 

findMediaByName delegates to MediaRepository.findMediaByName;

 

List<@Media> findMediaByCharacter(Long libraryId,

String characterName);

 

findPersonByName delegates to PersonService.findPersonByName;

}

 

对于ServiceSculptor将生成如下内容:

ü  服务端样板代码, 接口和实现类;

ü  访问仓库和其他服务的代理机制;

ü  JAX-WS Web Services

ü  Data Transfer Objects (DTO) with JAXB annotations

ü  消费端样板代码;

ü  错误处理和日志;

ü  Spring配置文件和XML配置文件;

ü  JUnit 测试类;

ü  可视化类图;

ü  可以点击预览的HTML格式化文档;

 

在服务实现过程中,可以向服务手动添加业务行为。你可以很轻松的实现一个仓库或其他服务的代理,而这一起只需在DSL原始文件中对业务行为操作的名字进行一下申明,返回类型和参数将会移植到代理的操作之中。

Sculptor也可以生成消息的消费,这是由EJB Message Driven Beans为纯粹的EJB3目标实现。

存储仓库层

存储仓库repositories封装了所有从数据库查询获取Domain Object的技术细节,同样也可以用于持久化新对象和删除对象。

域对象的仓库接口遍布于整个域对象,它提供了访问域对象的根源数据中心接口。

Repository MediaRepository {

int getNumberOfMovies(Long libraryId) delegates to AccessObject;

save;

findByQuery;

List<@Media> findMediaByName(Long libraryId, String name);

}

 

对于RepositoriesSculptor将生成如下内容:

ü  仓库样板代码

ü  Spring注解和XML配置文件

ü  泛型化的操作

n  FindById

n  findAll

n  findByKey

n  findByQuery

n  findByCondition

n  save

n  delete

仓库的默认实现包含了一个Access Objects访问对象的实现类。它意图实现域对象与数据访问层的分离解耦。仓库更加贴近于业务领域,而Access Objects访问对象更贴近于数据访问层。JPA/Hibernate 的描述代码位于Access Objects访问对象中,并不在仓库代码中。当然,你也可以选择跳过分离,将所有东西都在仓库代码中实现。

诸如上述特点,Sculptor运行时框架生成了很多泛型的操作,这些泛型操作对于仓库的通讯至关重要。然而,泛型操作不会自动仓库中添加,必须定义在DSL中,但这非常的简单!

 

CRUD GUI

胖领域模型是Sculptor的核心,但Sculptor仍然提供了前端到后端的实现。实现前端到后端到目的在于管理传统领域对象的新增、读取、修改和删除操作,同样在领域对象之间的关联也有良好的支持表现。这里提供了三种不同实现方式以供选择:

ü  Web Application with JSF and Spring WebFlow

ü  Rich Internet Application with GWT Smartclient

ü  Rich Client with Eclipse RCP


 

定制化Customization

这部分内容将视图扮演如何说明Sculptor能够进行定制开发。更多内容可以参考Sculptor Developer’s Guide

Sculptor并不是万能的适用于所有产品。尽快大部分情况下运用Sculptor进行大多数系统开发是一个不错的选择,但迟早还是需要定制化的开发内容。很多功能可以轻松的修改属性文件或AOP实现,而还有一部分内容的修改将需要更多的努力。尽管如此,Sculptor承认并不能解决所有问题,但是其设计框架和文档都使得开发者很容易的可以全面的控制这项工具。

对实现框架的支持

默认情况下,最终技术实现框架的主要包括如下:

ü  JPA with Hibernate as provider

ü  Spring with annotations, no EJBs

ü  Web CRUD GUI client with JSF, Facelets, and Spring Web Flow

ü  In-memory Hsqldb as database

ü  Deployment as war in Jetty

默认的实现对开发环境提供了一个良好的开始,它仅仅只依赖一些很少的外部软件,如数据库和中间件服务器。

对于这些技术领域,Sculptor还提供了一些其他可选择替代的技术支持,如:



内部设计

Sculptor 基于XtextXpand实现,并且是一个基于Apache 2 License的开源软件。


 

1.         开发者使用DSL编辑器的插件编辑应用的描述model.btdesign文件,如:实体模型的源文件进行代码生成流程。DSL的约束限制在编辑时进行校验;

2.         在生成应用描述代码的过程中,workflow.oaw将会被执行,它不会包含太多内容;

3.         它将会调用描述了代码生成流程的sculptorworkflow.oaw文件;

4.         DSL模型将会被转换成为一个被定义在sculptormetamodel.ecore原型的模型,这个原型将定义在EMF Ecore中;

5.         进行约束限制的校验;

6.         模型将会被再次转换,这是它将修改增加一些默认值;

7.         再次进行约束限制的校验;

8.         最后,真实的Java代码和配置文件生成了,它基于使用XPand语言编写的,定义在代码生成模板之中。这些模板的实际值将由XtendJava帮助类完成;

9.         原始技术的属性不属于DSL或者元数据,它将被模板和帮助类使用;

属性文件

你可以定制化修改简单的配置属性文件,如:

ü  替换整个或者部分运行时框架;

ü  新增客户化的泛型Access Objects

ü  可以很容易的更换选择拆开即用的数据库,如MySQLOraclePostgreSQL

ü  定义DSL类型与数据库类型、Java类型的映射文件;

ü  包名重命名;

ü  在仓库中使用JPA/Hibernate的支持替代Access Objects

修改代码生成模板

实际的代码生成使用XPand完成,这是一个简单而强大的模板语言。这些模板可以通过很精简的定义被很好描述结构化,例如方法。

你可以在Xpand中使用AOPAspect-Oriented Programming)修改代码生成模板,你可以覆盖原始模板文件的定义。例如如果你需要替换UUID的生成,那么:

«IMPORT sculptormetamodel

2018-06-17 20:47:18 FS1360472174 阅读数 2117

摘要

习惯了MVC模式,习惯了敏捷开发,习惯了了小步快跑,还适合谈论领域驱动开发吗。领域开发是否就是慢节奏的开发,
本文结合自己的开发经历,和大家聊聊这个话题。

#一.业务代码是如何写烂的#
java web开发通常都是mvc模式,从早期的ssh逐渐到Spring+ Mybatis。所以通常一个工程的项目结构图就是

  • controller
  • service
  • manager
  • dao

问题1: bean的职责不清

对应的bean就是
PO/DO(Persistence Object/Data Object):与数据库表结构一一对应,通过 DAO 层向上传输数据源对象
DTO(Data Transfer Object):数据传输对象,Service 和 Manager 向外传输的对象。
BO(Business Object): 业务对象。可以由 Service 层输出的封装业务逻辑的对象。一般不需要,
Query:数据查询对象,各层接收上层的查询请求。
VO(View Object): contoller层对外提供的

一般至少有PO/DTO/VO 三层结构,但是对于有些项目结构,业务比较简单,就是CRUD,有些开发人员就只有两层,controller到dao层就完了,然后bean的定义也特别混乱,随意创建,导致bean漫天飞,通常除了自己明白其中的含义,其他人都不明白,复用性极差。

问题2:面向过程的设计
此外 bean中都是属性,除了equals方法就都没有了。虽然有接口和实现,但是按照这样一套写出来的代码基本上和面向过程写的代码没有什么区别。这种开发方式bean类只有属性,没有行为。这样就会导致某一个实体的变更会散落在各个service中,而不是这个领域实体中。

问题3:不考虑业务模型
现在都是敏捷开发,导致开发人员也变得浮躁了,不分析或者草率分析需求,拿到就是干,随着业务迭代,开发人员增加,每个人各写一套,关于一个名词的定义都能有好几套写法,sql查询可能会分散到好多repo中,相同的sql可能会在不同的地方写上好几遍。关键是发现之前的模型定义错了,数据库的ER图设计有问题,仍然不会去更改,因为总是有新的需求会来,然后拼了命的做需求,留下一堆烂代码无法维护,最后连自己都不想看。

二. 领域模型是如何发挥作用的

比如说一个平台,一开始只有一种用户身份,后来平台做大了,开始做交易了,区分出了商家了,和买家了。产品提了个需求开发一个商家入驻流程,吭哧吭哧开发完了。一段时间后,开始需要有中间推销商了,一部分买家可以变成中间推销商了,那这时候产品跟你说,不用搞入驻流程,直接赋予原来的买家额外属性,来区分是否可以推销商品。这时候你能同意吗?

当然不行了,因为这个业务需求本质是就是交易,有买家,有卖家,有中间商。他们属于交易维度的不同实体,是同一个层次的,而用户则是不同的层次。一开始产品只会有需求说判断是中间商就可以,没有其他的了。等你在用户实体上加了一堆属性后,过了一段时间后,产品就会来跟你说,不好意思,哥们,卖家想管理中间商,需要中间商提供一些资质,你再帮我加几个属性。然后你的用户实体的模型开始无限扩张的模式了。对于产品来说,他是无所谓的,快速上线验证,验证了不行,换另外一条路,但是作为开发就被坑的天天加班了。

所以领域模型可以帮你解决,通常一些对于一些通用的领域,你可能很好找到对应模型设计,比如说订单,商品,抽奖,优惠券。但是这也需要你去阅读相关的产品文档,领域设计,当然如果你有一个靠谱的产品,你可能会轻松很多。而有一些不是通用领域的,所以需要你与你行业领域专家去深入聊,才能设计的出来领域模型,另外一点就是通常在一开始,会有些领域模型设计的有问题,需要不断的思考,纠正。

这里写图片描述

2011-04-02 21:52:24 huashuizhuhui 阅读数 31
聚合(Aggregation):
这是一种松散的对象间的关系.举个例子:计算机和他的外围设备就是一例.
用来表示拥有关系或者整体与部分的关系。
组合(Composition):
这是一种非常强的对象间的关系,举个例子,树和它的树叶之间的关系.
在一个合成里,部分与整体的生命周期都是一样的。一个合成的新对象完全拥有对其组成部分的支配权。包括他们的创建和毁灭。

这两个非常的相似,
聚合:
• 聚合有时能够不依赖部分而存在,有时又不能
• 部分可以独立于聚合而存在
• 如果有一部分遗失,聚合会给人一种不完全的感觉
• 部分的所有权可以由几个聚合来共享,比如打印机
[通过接口设置]
组成:
• 部分某一时刻只能属于某一个组成
• 组成唯一的负责处理它的所有部分--这就意味着负责他们的创建与销毁
• 倘若对于部分的职责由其他对象来承担的话,组成也就可以放松这些职责。
• 如果组成销毁的话,它必须销毁所有的部分,或者把负责他们的权利转移给其他对象。
[通过代码组合]

模型驱动设计(Model-DrivenDesign)抛弃了分裂分析模型与设计的做法,使用单一的模型来满足这两方面的要求。这就是领域模型!

领域模型把分析和设计放在一起,单一的领域模型同时满足分析原型和软件设计,如果一个模型实现时不实用,重新寻找新模型。[只用单一的模型来满足分析与设计,不能满足就重新寻找模型]如何寻找模型?模型又是一个什么?
根据Eric的理论,业务层将细分为两个层次:应用层和领域层。

应用层:定义软件可以完成的工作,并且指挥具有丰富含义的领域对象来解决问题,保持精练;不包括业务规则或知识,无业务情况的状态;Action[这个代码更改的概率为0]

领域层:负责表示业务概念、业务状态的信息和业务规则,是业务软件核心。层次之间必须清晰分离,每个层都是内聚的,并且只依赖它的下层.[在领域 层会建立模型,比如用户模型,保存了用户的所有属性,用户所有的功能,全部面向接口,依赖下层实现,也就是以抽象的方式建立模型,就也就是在分析和设计的 时候设置的用户模型,比如以下类图]


分析与设计[设置顶级接口很重要]
用户顶级接口 User
Java代码
1. UserInfo getUserInfo() --得到用户信息:用户属性
2. void Speak() --说话:用户功能
3. void Walk() --走路:用户功能


看到这里会不会觉得用户接口很简单,很简洁.[爆露给外部调用的方法,在设计的时候已经确定]

用户接口的功能内聚,用户模型保存用户属性,所有功能面向接口,依赖下层实现.
接口抽象类 AbstratorUser
Java代码
1. UserInfo userInfo; --用户属性
2.
3. //用户操作对象.
4. Abstrator UserOperator getUserOperator() –用户操作对象信息,依赖下层
5.
6. //overwriter
7. Speak(){
8. getUserOperator ().Speak();
9. }
10.
11. //overwriter
12. Walk(){
13. getUserOperator ().Walk();
14. }
15. //用户信息在初始的时候留给子类设置
16. Void setUserInfo(UserInfo userInfo){
17. This.userInfo=userInfo
18. }
19.
20. //overwriter
21. UserInfo getUserInfo(){
22. Return userInfo;
23. }

[对业务逻辑的初步封装,蓝色部分是重写方法,而红色部分是需要依赖下层建筑的]

以上为设计阶段完成的,而下层建筑就交给开发工程师来完成,不同的开发工程师的水平和风格可能不一样,但是完全分离的方式使各模块[用户信息,用户操作]都能够正常运行,耦合为0. [这个代码更改的概率为0]

现在Action层代码更改为0,接口层和抽象层代码修改为0,那么当用户逻辑变的时候,只需要修改下层建筑就可以了!.

UserInfo为用户信息,这是一个实体类,其各对象的属性采用组合的方式来连接
Address,Account,Family.如果需要添加一些Friend等信息很方便,与其他模块不会有耦合.
UserOperator为用户操作对象,这是一个接口,依赖下层建筑.如果用户Speak的方式从中文变成英文了,只需要修改下层建筑就可以了.

这就是DDD,领域驱动开发,只依赖下层建筑,而下层建筑就是最简单的增删改查操作.也就是业务最需要变动的地方,领域驱动可以把一个项目分成一 个域,而这个域能够包含这个项目的所有信息,其实DDD并不算新的技术,他只是提供了一个概率,分析与设计的组合可以把业务逻辑理清,减少上层建筑的缺 陷,而下层建筑是可以随机换的,上层建筑一般是基本接口和抽象,只要业务逻辑没有分析出错,就不需要改动.而DDD把层分成两层,应用层和领域层,也是从 整体考虑,应用层调用领域层的接口,其实领域层也可以分层,他只是让系统的策划更简单,不用考虑那么多层,系统都是从简单到复杂,这相当于数据挖掘技术, 一张图很简单,再挖一下也很简单,再挖一下也很简单,应用不同的项目,不同的模块,基于接口和抽象的设计,并能从全局考虑就是DDD的思想.

现在来回答领域模型是什么:
领域模型是对领域内的概念类或现实世界中对象的可视化表示。
现实中的人 User
人的属性 getUserInfo()
人说话 Opertator.Speak()
人走路 Operator.Walk()
如何寻找模型:
在业务专家和设计专家一些交流的时候,设计专家能够从业务专家的描述中抽取出实体及实体间的关系[泛化、依赖和关联,关联又分了一般关联、聚合、组合等等]
2018-03-16 21:32:00 beyond59241 阅读数 92

本文作者是组内同事 杜宁,目前负责美团外卖活动管理模块业务。

什么是领域驱动模型?

2004年Eric Evans 发表《领域驱动设计——软件核心复杂性应对之道》(Domain-Driven Design –Tackling Complexity in the Heart of Software),简称Evans DDD,领域驱动设计思想进入软件开发者的视野。领域驱动设计分为两个阶段:

  • 1、以一种领域专家、设计人员、开发人员都能理解的通用语言作为相互交流的工具,在交流的过程中发现领域概念,然后将这些概念设计成一个领域模型;

  • 2、由领域模型驱动软件设计,用代码来实现该领域模型;

简单地说,软件开发不是一蹴而就的事情,我们不可能在不了解产品(或行业领域)的前提下进行软件开发,在开发前,通常需要进行大量的业务知识梳理,而后到达软件设计的层面,最后才是开发。而在业务知识梳理的过程中,我们必然会形成某个领域知识,根据领域知识来一步步驱动软件设计,就是领域驱动设计的基本概念。而领域驱动设计的核心就在于建立正确的领域驱动模型。

5401760-370d2ebfc07bb2f9.png
image.png

传统软件开发与贫血模型

传统的开发思想

传统开发四层架构

5401760-34d2eeb9cbcd7105.png
image.png

在传统模型中,对象是数据的载体,只有简单的getter/setter方法,没有行为。以数据为中心,以数据库ER设计作驱动。分层架构在这种开发模式下,可以理解为是对数据移动、处理和实现的过程。

以商家活动为例,首先设计数据库表配置

5401760-ebbd63c9669430ca.png
image.png

设计WmActPoi对象,只有简单的get和set属性方法

public class WmActPoi {
 
    private Long id;
 
    private String wmPoiId;
 
    private Integer startTime;
 
    private Integer endTime;
    public Long getId() {
        return id;
    }
 
    public void setId(Long id) {
        this.id = id;
    }
 
    public Integer getWmPoiId() {
        return wmPoiId;
    }
 
    public WmActPoiDB setWmPoiId(Integer wmPoiId) {
        this.wmPoiId = wmPoiId;
    }
    ......
}

Service层代码实现

class WmActPoiService {
    saveWmActPoi();   //保存活动
    checkWmActPoi();   //活动校验
    ....
}
  
class WmActPoiQueryService {
    queryWmActPoi();   //查询活动
    queryWmActPoiByWmPoiId();   //根据门店查询活动
    ....
}

可以看到,业务逻辑都是写在Service中的,WmActPoi充其量只是个数据载体,没有任何行为,是一种贫血模型。简单的业务系统采用这种贫血模型和过程化设计是没有问题的,但在业务逻辑复杂了,业务逻辑、状态会散落到在大量方法中,原本的代码意图会渐渐不明确,我们将这种情况称为由贫血症引起的失忆症

5401760-dca70a6662cc6ea3.png
image.png

传统架构的特点:

  • a. 以数据库为中心

  • b. 贫血模型

  • c. 业务逻辑散落在大量的方法中

  • d. 当系统越来越复杂时,开发时间指数增长,维护成本很高

DDD设计思想

采用DDD的设计思想,业务逻辑不再集中在几个大型的类上,而是由大量相对小的领域对象(类)组成,这些类具备自己的状态和行为,每个类是相对完整的独立体,并与现实领域的业务对象映射。领域模型就是由这样许多的细粒度的类组成。

建立领域知识(Build Domain Model)

说了这么多领域模型的概念,到底什么是领域模型呢?作为一个软件开发者,我们很难在对一个领域不了解的情况下着手开发,所以我们首先需要和领域专家沟通,建立领域只是。以飞机航行为例子:

现要为航空公司开发一款能够为飞机提供导航,保证无路线冲突监控软件。那我们应该从哪里开始下手呢?根据DDD的思路,我们第一步是建立领域知识:作为平时管理和维护机场飞行秩序的工作人员来说,他们自然就是这个领域的专家,我们第一个目标就是与他们沟通,也许我们并不能从中获取所有想要的知识,但至少可以筛选出主要的内容和元素。你可能会听到诸如起飞,着陆,飞行冲突,延误等领域名词,让们从一个简单的例子开始:

  • 起点->飞机->终点

这个模型很直接,但有点过于简单,因为我们无法看出飞机在空中做了什么,也无法得知飞机怎么从起点到的终点,那么如此似乎会好些:

  • 飞机->路线->起点/终点

    既然点构成线,那何不:

  • 飞机->路线->points(含起点,终点)

    这个过程,是我们不断建立领域知识的过程,其中的重点就是寻找领域专家频繁沟通,从中提炼必要领域元素。

通用语言(Ubiquitous Language)

上面的例子的确看起来简单,但过程并非容易:我们(开发人员)和领域专家在沟通的过程中是存在天然屏障的:我们满脑子都是类,方法,设计模式,算法,继承,封装,多态,如何面向对象等等;这些领域专家是不懂的,他们只知道飞机故障,经纬度,航班路线等专业术语。

所以,在建立领域知识的时候,我们(开发人员和领域专家)必须要交换知识,知识的范围范围涉及领域模型的各个元素,如果一方对模型的描述令对方感到困惑,那么应该立刻换一种描述方式,直到双方都能够接受并且理解为止。在这一过程中,就需要建立一种通用语言,作为开发人员和领域专家的沟通桥梁。

可如何形成这种通用语言呢?其实答案并不唯一,确切的说也没有什么标准答案。

(a) UML

利用UML可以清晰的表现类,并且展示它们之间的关系。但是一旦聚合关系复杂,UML叶子节点将会变的十分庞大,可能就没有那么直观易懂了。最重要的是,它无法精确的描述类的行为。为了弥补这种缺陷,可以为具体的行为部分补充必要说明(可以是标签或者文档),但这往往又很耗时,而且更新维护起来十分不便。

(b) 文档/绘图

文档耗时很长,可能不久就要变化,为模型从一开 始到它达到比较稳定的状态会发生很多次变化, 可能在完成之前它们就已经作废了。对于复杂系统,绘图容易混乱。

(c) 伪代码

极限编程推荐这么做,但是使用难度大

领域驱动设计的分层架构和构成要素

5401760-70ce52cd70e2684a.png
image.png

一个通用领域驱动设计的架构性解决方案包含4 个概念层:

5401760-0d55a15858e2ef6b.png
image.png
5401760-f390d006ce1d9abc.png
image.png

层结构的划分是很有必要的,只有清晰的结构,那么最终的领域设计才宜用,比如用户要预定航班,向Application Layer的service发起请求,而后Domain Layler从Infrastructure Layer获取领域对象,校验通过后会更新用户状态,最后再次通过Infratructure Layer持久化到数据库中。

领域驱动模型的一些要素

5401760-589f63348b8953b7.png
image.png

实体(Entity) & 值对象(Value Object)

实体与面向对象中的概念类似,在这里再次提出是因为它是领域模型的基本元素。在领域模型中,实体应该具有唯一的标识符,从设计的一开始就应该考虑实体,决定是否建立一个实体也是十分重要的。

值对象和我们说的编程中数值类型的变量是不同的,它仅仅是没有唯一标识符的实体,比如有两个收获地址的信息完全一样,那它就是值对象,并不是实体。值对象在领域模型中是可以被共享的,他们应该是“不可变的”(只读的),当有其他地方需要用到值对象时,可以将它的副本作为参数传递。

服务(Services)

当我们在分析某一领域时,一直在尝试如何将信息转化为领域模型,但并非所有的点我们都能用Model来涵盖。对象应当有属性,状态和行为,但有时领域中有一些行为是无法映射到具体的对象中的,我们也不能强行将其放入在某一个模型对象中,而将其单独作为一个方法又没有地方,此时就需要服务.

服务是无状态的,对象是有状态的。所谓状态,就是对象的基本属性:高矮胖瘦,年轻漂亮。服务本身也是对象,但它却没有属性(只有行为),因此说是无状态的。

服务存在的目的就是为领域提供简单的方法。为了提供大量便捷的方法,自然要关联许多领域模型,所以说,行为(Action)天生就应该存在于服务中。

服务具有以下特点:

  • a)服务中体现的行为一定是不属于任何实体和值对象的,但它属于领域模型的范围内
  • b)服务的行为一定涉及其他多个对象
  • c)服务的操作是无状态的

模块(Moudles)

对于一个复杂的应用来说,领域模型将会变的越来越大,以至于很难去描述和理解,更别提模型之间的关系了。模块的出现,就是为了组织统一的模型概念来达到减少复杂性的目的。而另一个原因则是模块可以提高代码质量和可维护性,比如我们常说的高内聚,低耦合就是要提倡将相关的类内聚在一起实现模块化。

模块应当有对外的统一接口供其他模块调用,比如有三个对象在模块a中,那么模块b不应该直接操作这三个对象,而是操作暴露的接口。模块的命名也很有讲究,最好能够深层次反映领域模型。

聚合(Aggregates)

聚合表示一组领域对象(包括实体和值对象),用来表述一个完整的领域概念。而每个聚合都有一个根实体,这个根实体又叫做聚合根。举个简单的例子,一个电脑包含硬盘、CPU、内存条等,这一个组合就是一个聚合,而电脑就是这个组合的聚合根。在聚合中,根是唯一允许外部对象保持对它的引用的元素,而边界内部的对象之间则可以互相引用。除根以外的其他Entity都有本地表示,但这些标识只有在聚合内部才需要加以区别,因为外部对象除了根Entity之外看不到其他对象。

5401760-2f95977c4897d122.png
image.png

工厂(Factory)

一个对象的创建可能是它自身的主要操作,但是复杂的组装操作不应该成为被创建对象的职责。组合这样的职责会产生笨拙的设计, 也很难让人理解。因此,有必要引入一个新的概念,这个概念可以帮助封装复杂的对象创建过程,它就是工厂(Factory)。工厂用来封装对象创建所必需的知识,它们对创建聚合特别有用。当聚合的根建立时,所有聚合包含的对象将随之建立。

5401760-410dc537d89d3d9c.png
image.png

资源库(Repositories)

资源库的是封装所有获取对象引用所需的逻辑。领域对象不需处理基础设施,以得到领域中对其他对象的所需的引用。只需从资源库中获取它们,于是模型重获它应有的清晰和焦点。

资源库会保存对某些对象的引用。当一个对象被创建出来时,它可以被保存到资源库中,然后以后使用时可从资源库中检索到。如果客户程序从资源库中请求一个对象,而资源库中并没有它,就会从存储介质中获取它。换种说法是,资源库作为一个全局的可访问对象的存储点而存在。

Repository的接口应当采用领域通用语言。作为客户端,不应当知道数据库实现的细节。

Repository和DAO的作用类似,二者的主要区别:

DAO是比Repository更低的一层,包含了如何从数据库中提取数据的代码。

Repository以“领域”为中心,所描述的是“领域语言”。Repository把ORM框架与领域模型隔离,对外隐藏封装了数据访问机制。

5401760-3d46651fc229e72b.png
image.png
public
interface AccountRepository {
    Account findAccount(String accountId);
    void addAccount(Accountaccount);
}

工厂和资源库之间存在一定的关系。它们都是模型驱动设计中的模式,它们都能帮助我们关联领域对象的生命周期。然而工厂关注的是对象的创建,而资源库关心的是已经存在的对象。资源库可能会 在本地缓存对象,但更常见的情况是需要从一个持久化存储中检索 它们。对象可以用构造函数创建,也可以被传递给一个工厂来构 建。从这个原因上讲,资源库也可以被看作一个工厂,因为它创建对象。不过它不是从无到有创建新的对象,而是对已有对象的重建。我们将不把资源库视为一个工厂。工厂创建新的对象,而资源库应该是用来发现已经创建过的对象。当一个新对象被添加到资源库时,它应该是先由工厂创建过的,然后它应该被传递给资源库以便将来保存它,见下面的例子:

5401760-0c9c20ba68afa234.png
image.png

持续集成与模型一致性

规约(Factory)

规约是一种布尔断言。


5401760-0e88e1a193b68ae0.png
image.png

规约是业务规则的 部分 理论上规约类中的方法只有个:isSatisfiedBy(Object obj)。

规约用来测试对象是否满足某种条件,用来进行对象查询,也可以作为某个对象的创建条件。

单一规约规则。多个规约通过组合表现复杂的规约。

5401760-b07c54f3ec0a9ccf.png
image.png
5401760-33341191cc78d729.png
image.png
5401760-968e4a9b39fda8cd.png
image.png
5401760-3e6906e2a80ca0f7.png
image.png

限界上下文(Bounded Context)

明确的定义模型所应用的上下文。根据团队的组织、软件系统的功能和物理表现(代码数据库)来设置模型的边界。在这些边界中严格保持模型的一致性,而不要受到边界之外问题的混淆。每个团队负责自己的模型,并为其他模型提供服务。

上下文映射(Context Map)

一个企业应用有多个模型,每个模型有自己的界定的上下文。建议用上下文作为团队组织的基础。在同一个团队里的人们能更容易地 沟通,也能很好地将模型集成和实现。但是每个团队都工作于自己 的模型,所以最好让每个人都能了解所有的模型。上下文映射(Context Map)是指抽象出不同界定上下文和它们之间关系的文 档,它可以是像下面所说的一个试图(Diagram),也可以是其他任 何写就的文档。详细的层次各有不同。它的重要之处是让每个在项 目中工作的人都能够得到并理解它。

5401760-2e38fb2293c4dd41.png
image.png

共享内核(Shared Kernel)

5401760-8b8d43fb8b230afc.png
image.png

总结

领域驱动设计的核心是领域模型,这一方法论可以通俗的理解为先找到业务中的领域模型,以领域模型为中心驱动项目的开发。而领域模型的设计精髓在于面向对象分析,在于对事物的抽象能力,一个领域驱动架构师必然是一个面向对象分析的大师。

在面向对象编程中讲究封装,讲究设计低耦合,高内聚的类。而对于一个软件工程来讲,仅仅只靠类的设计是不够的,我们需要把紧密联系在一起的业务设计为一个领域模型,让领域模型内部隐藏一些细节,这样一来领域模型和领域模型之间的关系就会变得简单。这一思想有效的降低了复杂的业务之间千丝万缕的耦合关系。

DDD开发案例

超市收银业务

领域驱动设计在互联网业务开发中的实践

本文作者是组内同事 杜宁,目前负责美团外卖活动管理模块业务。


欢迎关注 高广超的简书博客 与 收藏文章 !
欢迎关注 头条号:互联网技术栈

个人介绍:

高广超:多年一线互联网研发与架构设计经验,擅长设计与落地高可用、高性能、可扩展的互联网架构。

本文首发在 高广超的简书博客 转载请注明!

5401760-f9ed3623b46b3500.png
简书博客
5401760-23fae55670a45dc4.png
头条号

DDD 战术模型之聚合

阅读数 1002

DDD领域驱动设计

阅读数 14

没有更多推荐了,返回首页