领域驱动DDD在签到场景落地案例之概念初识(一)

领域驱动(DDD)

了解领域驱动设计前我们不妨先问自己几个问题:
1、 领域驱动设计是什么?
2、 为什么要用领域领域驱动设计来指导开发软件?
3、 什么样的业务场景更适合领域驱动设计?

领域驱动设计近两年非常火热,相信做研发的同学或多或少都听过DDD、领域驱动设计等概念,有时候看过一些文章讲领域模型、限界上下文、聚合、实体等名称,但是把这些放到一起就不清楚怎么回事了。下面会从多个方面介绍领域驱动设计以及项目中如何使用。
第一步:聊聊领域驱动设计是什么
第二步:领域驱动常见的几种架构模式
第三步:为什么用领域驱动以及领域驱动如何指导开发
第四步:从真实案例《每日签到》项目用三层架构实现和领域驱动思想实现有什么不同
第五步:总结三层架构和领域驱动设计的异曲同工
第六步:什么样的业务更适合领域驱动设计

领域驱动设计是什么

系统在为业务服务,架构在为业务服务,研发也是在为业务服务,一切脱离业务的架构设计都是自娱自乐的。
架构演进:单体架构(C/S)、集中架构(三层)、微服务
软件架构模式的演进
C/S架构:通过客户端处理所有的业务功能直接和数据库连接,缺点也很明显,客户端需要用户安装,需要手动更新等问题。

B/S架构:浏览器和服务端交互,现在我们常用的软件都可以在浏览器直接访问了,比如看视频,看股票,看新闻等,该阶段在浏览器展示页面,服务端处理业务逻辑,再连接数据库就形成三层架构
集中式部署
微服务部署
微服务:在互联网早期业务并没有那么复杂,各种功能模块放到一起部署就没问题,随着业务增长,用户增长,对系统的要求越来越高。原来一两个人能开发的系统,现在需要几十上百人共同开发,每天都会有新的功能都会上线,如果还用all in one的方式不仅开发效率低,而且上线风险高,A仅改动了一个商品相关的功能,却要开发库存的同事也要监控系统上线以免出现问题。
对于上面开发问题、上线问题、系统性能问题等因素,需要将大系统合理的拆分成独立的小系统

领域驱动设计:领域驱动就是一套方法论,通过领域驱动设计方法论来定义领域模型,从而确定业务边界和应用边界,保证业务模型和代码模型的一致性

领域驱动设计是处理复杂领域的设计思想,把业务复杂性和技术复杂性分离,运用业务概念构建领域模型来控制业务复杂度。领域驱动不是架构,而是一种架构设计的方法论,通过边界划分把复杂的业务简单化,帮我们设计出清晰的领域和应用边界,可以容易的实现架构的演进

领域驱动包括战略设计和战术设计两个部分
战略设计是从业务视角出发,建立领域模型。划分领域边界,建立通用语言限界上下文,限界上下文可以作为微服务划分的边界

战术设计是从技术的视角出发,通过领域模型,完成软件的落地。包括:聚合根、实体、值对象等的设计与实现

DDD与微服务的关系
DDD是一种架构设计方法论,微服务是一种架构风格,他们关注的点不一样
DDD主要关注:从业务领域视角划分领域边界,构建通用语言进行沟通,通过业务抽象,构建领域模型,实现业务架构和系统架构的统一
微服务主要关注:微服务的独立开发、测试、运维、部署等。

领域设计中常见的名词

领域专家

领域专家是某个业务域的专家,可以由运营、产品、技术、项目经历等角色组成

通用语言

通用语言是团队共享的语言,一个业务名词表示特有的含义,通用语言确定后后期是可以提升业务和技术的沟通效率的,并且表达意思也更准确,看下面一个例子:

示例:比如我们遇到一个歧义的地方,在签到活动里面“签到记录”这个名词,用户签到成功之后,会把用户这当天的一条签到信息保存数据库里,可以称为一个签到记录。那么还有一个场景,比如活动是按照周签到的,用户周一到周日这个签到过程中,系统会缓存这个签到信息,这个签到信息有别于数据库中的一条数据,因位它包括了7天的签到信息,主要有日期、当天是否签到、是否补签、奖励是否发送等,那他也可以交一个签到记录,如果我们在沟通的时候都用签到记录这个词,这两个场景的就不太好区分。

缓存中1-7天的记录我们叫,签到日历
数据库持久化的数据叫,签到记录

签到,这个词本身就是动词含义,比如用户说“我进入金融签到”,那么这里的签到是指打开了签到页面,还是执行了签到操作呢?
围绕签到名词就有很多示意:
签到页面:指用户进入了每日签到活动首页
执行签到操作:指用户点击了签到按钮,给出明确成功或失败的反馈
签到奖励:指用户签到成功后发送的奖励,有别于做任务获得的奖励

上面那句话改成,我打开了金融APP的每日签到页面,是不是意思就很明确了

限界上下文

在一个领域/子域中,我们会创建一个概念上的领域边界,在这个边界中,任何领域对象都只表示特定于该边界内部的确切含义。这样边界便称为限界上下文。限界上下文和领域具有一对一的关系。
  举个例子,同样是一本书,在出版阶段和出售阶段所表达的概念是不同的,出版阶段我们主要关注的是出版日期,字数,出版社和印刷厂等概念,而在出售阶段我们则主要关心价格,物流和发票等概念。我们应该怎么办呢,将所有这些概念放在单个Book对象中吗?这不是DDD的做法,DDD有限界上下文将这两个不同的概念区分开来。
  从物理上讲,一个限界上下文最终可以是一个DLL(.NET)文件或者JAR(Java)文件,甚至可以是一个命名空间(比如Java的package)中的所有对象。但是,技术本身并不应该用来界分限界上下文。
  将一个限界上下文中的所有概念,包括名词、动词和形容词全部集中在一起,我们便为该限界上下文创建了一套通用语言。通用语言是一个团队所有成员交流时所使用的语言,业务分析人员、编码人员和测试人员都应该直接通过通用语言进行交流。
  对于上文中提到的各个子域之间的集成问题,其实也是限界上下文之间的集成问题。在集成时,我们主要关心的是领域模型和集成手段之间的关系。比如需要与一个REST资源集成,你需要提供基础设施(比如Spring 中的RestTemplate),但是这些设施并不是你核心领域模型的一部分,你应该怎么办呢?答案是防腐层,该层负责与外部服务提供方打交道,还负责将外部概念翻译成自己的核心领域能够理解的概念。当然,防腐层只是限界上下文之间众多集成方式的一种,另外还有共享内核、开放主机服务等。限界上下文之间的集成关系也可以理解为是领域概念在不同上下文之间的映射关系,因此,限界上下文之间的集成也称为上下文映射图。

上下文映射图

团队使用上下文映射图来理解项目的范围

领域和子域(Domain/Subdomain)

既然是领域驱动设计,那么我们主要的关注点理所当然应该放在如何设计领域模型上,以及对领域模型的划分。
  领域并不是多么高深的概念,比如,一个保险公司的领域中包含了保险单、理赔和再保险等概念;一个电商网站的领域包含了产品名录、订单、发票、库存和物流的概念。这里,我主要讲讲对领域的划分,即将一个大的领域划分成若干个子域。
  在日常开发中,我们通常会将一个大型的软件系统拆分成若干个子系统。这种
  划分有可能是基于架构方面的考虑,也有可能是基于基础设施的。但是在DDD中,我们对系统的划分是基于领域的,也即是基于业务的。
  于是,问题也来了:首先,哪些概念应该建模在哪些子系统里面?我们可能会发现一个领域概念建模在子系统A中是可以的,而建模在子系统B中似乎也合乎情理。第二个问题是,各个子系统之间的应该如何集成?有人可能会说,这不简单得就像客户端调用服务端那么简单吗?问题在于,两个系统之间的集成涉及到基础设施和不同领域概念在两个系统之间的翻译,稍不注意,这些概念就会对我们精心创建好的领域模型造成污染。
如何解决?答案是:限界上下文和上下文映射图。

聚合(Aggregate)

聚合是实体的升级,是由一组与生俱来就密切相关实体和值对象组合而成的,整个组合的最上层实体就是聚合

聚合可能是DDD中最难理解的概念 ,之所以称之为聚合,是因为聚合中所包含的对象之间具有密不可分的联系,他们是内聚在一起的。比如一辆汽车(Car)包含了引擎(Engine)、车轮(Wheel)和油箱(Tank)等组件,缺一不可。一个聚合中可以包含多个实体和值对象,因此聚合也被称为根实体。聚合是持久化的基本单位,它和资源库(请参考下文)具有一一对应的关系。
  既然聚合可以容纳其他领域对象,那么聚合应该设计得多大呢?这也是设计聚合的难点之一。比如在一个博客(Blog)系统中,一个用户(User)可以创建多个Blog,而一个Blog又可以包含多篇博文(Post)。在建模时,我们通常的做法是在User对象中包含一个Blog的集合,然后在每个Blog中又包含了一个Post的集合。你真的需要这么做吗?如果你需要修改User的基本信息,在加载User时,所有的Blog和Post也需要加载,这将造成很大的性能损耗。诚然,我们可以通过延迟加载的方式解决问题,但是延迟加载只是技术上的实现方式而已。导致上述问题的深层原因其实在我们的设计上,我们发现,User更多的是和认证授权相关的概念,而与Blog关系并不大,因此完全没有必要在User中维护Blog的集合。在将User和Blog分离之后,Blog也和User一样成为了一个聚合,它拥有自己的资源库。问题又来了:既然User和Blog分离了,那么如果需要在Blog中引用User又该怎么办呢?在一个聚合中直接引用另外一个聚合并不是DDD所鼓励的,但是我们可以通过ID的方式引用另外的聚合,比如在Blog中可以维护一个userId的实例变量。
  User作为Blog的创建者,可以成为Blog的工厂。放到DDD中,创建Blog的功能也只能由User完成。
  综上,对于“创建Blog”的用例,我们可以通过以下方法完成:

public class Customer {
    private String email;

    public void setEmail(String email) {
        this.email = email;
    }
} 

在上例中,业务用例通过BlogApplicationService应用服务完成,在用例方法createBlog()中,首先通过User的资源库得到一个User,然后调用User中的工厂方法createBlog()方法创建一个Blog,最后通过BlogRepository对Blog进行持久化。整个过程构成了一次事务,因此createBlog()方法标记有@Transactional作为事务边界。
  使用聚合的首要原则为在一次事务中,最多只能更改一个聚合的状态。如果一次业务操作涉及到了对多个聚合状态的更改,那么应该采用发布领域事件(参考下文)的方式通知相应的聚合。此时的数据一致性便从事务一致性变成了最终一致性(Eventual Consistency)。

聚合根(Aggregate Root)

就是一个聚合中的核心

实体(Entity)

每个实体是唯一的,并且可以相当长的一段时间内持续地变化。我们可以对实体做多次修改,故一个实体对象可能和它先前的状态大不相同。但是,由于它们拥有相同的身份标识,他们依然是同一个实体。例如一件商品在电商商品上下文中是一个实体,通过商品中台唯一的商品 id 来标示这个实体。

值对象(Value Object)

值对象用于度量和描述事物,当你只关心某个对象的属性时,该对象便可作为一个值对象。实体与值对象的区别在于唯一的身份标识和可变性。当一个对象用于描述一个事物,但是又没有唯一标示,那么它就是一个值对象。例如商品中的商品类别,类别就没有一个唯一标识,通过图书、服装等这些值就能明确表示这个商品类别。

在一个软件系统中,实体表示那些具有生命周期并且会在其生命周期中发生改变的东西;而值对象则表示起描述性作用的并且可以相互替换的概念。同一个概念,在一个软件系统中被建模成了实体,但是在另一个系统中则有可能是值对象。例如货币,在通常交易中,我们都将它建模成了一个值对象,因为我们花了20元买了一本书,我们只是关心货币的数量而已,而不是关心具体使用了哪一张20元的钞票,也即两张20元的钞票是可以互换的。但是,如果现在中国人民银行需要建立一个系统来管理所有发行的货币,并且希望对每一张货币进行跟踪,那么此时的货币便变成了一个实体,并且具有唯一标识(Identity)。在这个系统中,即便两张钞票都是20元,他们依然表示两个不同的实体。
  具体到实现层面,值对象是没有唯一标识的,他的equals()方法(比如在Java语言中)可以用它所包含的描述性属性字段来实现。但是,对于实体而言,equals()方法便只能通过唯一标识来实现了,因为即便两个实体所拥有的状态是一样的,他们依然是不同的实体,就像两个人的名字都叫张三,但是他们却是两个不同的人的个体。
  我们发现,多数领域概念都可以建模成值对象,而非实体。值对象就像软件系统中的过客一样,具有“创建后不管”的特征,因此,我们不需要像关心实体那样去关心诸如生命周期和持久化等问题。

领域服务(Domain Service)

领域中的服务层

你是否遇到过这样的问题:想建模一个领域概念,把它放在实体上不合适,把它放在值对象上也不合适,然后你冥思苦想着自己的建模方式是不是出了问题。恭喜你,祝贺你,你的建模手法完全没有问题,只是你还没有接触到领域服务(Domain Service)这个概念,因为领域服务本来就是来处理这种场景的。比如,要对密码进行加密,我们便可以创建一个PasswordEncryptService来专门负责此事。
  值得一提的是,领域服务和上文中提到的应用服务是不同的,领域服务是领域模型的一部分,而应用服务不是。应用服务是领域服务的客户,它将领域模型变成对外界可用的软件系统。
  领域服务不能滥用,因为如果我们将太多的领域逻辑放在领域服务上,实体和值对象上将变成贫血对象。

领域事件(Domain Event)

领域中的事件处理

在Eric的《领域驱动设计》中并没有提到领域事件,领域事件是最近几年才加入DDD生态系统的。
  在传统的软件系统中,对数据一致性的处理都是通过事务完成的,其中包括本地事务和全局事务。但是,DDD的一个重要原则便是一次事务只能更新一个聚合实例。然而,的确存在需要修改多个聚合的业务用例,那么此时我们应该怎么办呢?
  另外,在最近流行起来的微服务(Micro Service)的架构中,整个系统被分成了很多个轻量的程序模块,他们之间的数据一致性并不容易通过事务一致性完成,此时我们又该怎么办呢?
  在DDD中,领域事件便可以用于处理上述问题,此时最终一致性取代了事务一致性,通过领域事件的方式达到各个组件之间的数据一致性。
  领域事件的命名遵循英语中的“名词+动词过去分词”格式,即表示的是先前发生过的一件事情。比如,购买者提交商品订单之后发布OrderSubmitted事件,用户更改邮箱地址之后发布EmailAddressChanged事件。
  需要注意的是,既然是领域事件,他们便应该从领域模型中发布。领域事件的最终接收者可以是本限界上下文中的组件,也可以是另一个限界上下文。
领域事件的额外好处在于它可以记录发生在软件系统中所有的重要修改,这样可以很好地支持程序调试和商业智能化。另外,在CQRS架构的软件系统中,领域事件还用于写模型和读模型之间的数据同步。再进一步发展,事件驱动架构可以演变成事件源(Event Sourcing),即对聚合的获取并不是通过加载数据库中的瞬时状态,而是通过重放发生在聚合生命周期中的所有领域事件完成。

资源库(Repository)

资源库用于保存和获取聚合对象,在这一点上,资源库与DAO多少有些相似之处。但是,资源库和DAO是存在显著区别的。DAO只是对数据库的一层很薄的封装,而资源库则更加具有领域特征。另外,所有的实体都可以有相应的DAO,但并不是所有的实体都有资源库,只有聚合才有相应的资源库。
  资源库分为两种,一种是基于集合的,一种是基于持久化的。顾名思义,基于集合的资源库具有编程语言中集合的特征。举个例子,Java中的List,我们从一个List中取出一个元素,在对该元素进行修改之后,我们并不用显式地将该元素重新保存到List里面。因此,面向集合的资源库并不存在save()方法。比如,对于上文中的User,其资源库可以设计为:

public interface CollectionOrientedUserRepository {
    public void add(User user);
    public User userById(String userId);
    public List allUsers();
    public void remove(User user); 
} 

对于面向持久化的资源库来说,在对聚合进行修改之后,我们需要显式地调用sava()方法将其更新到资源库中。依然是User,此时的资源库如下:

public interface PersistenceOrientedUserRepository { 
    public void save(User user); 
    public User userById(String userId); 
    public List<User> allUsers(); 
    public void remove(User user); 
}

在以上两种方式所实现的资源库中,虽然只是将add()方法改成了save()方法,但是在使用的时候却是不一样的。在使用面向集合资源库时,add()方法只是用来将新的聚合加入资源库;而在面向持久化的资源库中,save()方法不仅用于添加新的聚合,还用于显式地更新既有聚合。

工厂(Factory)

创建对象和聚合的职责转移给单独的工厂对象完成,包括复杂对象的创建、对象的转换、对象数据重塑等。说是工厂实际和简单工厂、抽象工厂没太大关系,甚至不需要工厂类把复杂对象的创建直接写在实体里或聚合里。
工厂和仓储的区别是工厂只负责组装对象,而仓储则侧重从资源库取出数据,又重新存储到资源库
工厂有多种形式,可以是一个独立的 Factory 对象,也可以是聚合根上的工厂方法,也可以是领域服务

为什么需要工厂

  • 因为一个聚合对象包含关系比较复杂,聚合中有实体,实体中还包含实体或值对象。当创建一个复杂对象或聚合的过程很复杂并且暴露出了过多的内部结构时,我们则可以使用工厂进行封装。一个对象在它的声明周期中要承担大量的职责,如果再让复杂对象负责自身的创建,那么职责过载将会导致问题。
  • 我们设计好领域模型供客户方调用,但如果客户方也必须使用如何装配这个对象,则必须知道对象的内部结构。好比你去驾校学车,却得先学会发动机的原理。对客户方开发来说这是很不友好的。其次,复杂对象或者聚合当中的领域知识(业务规则)需要得到满足,如果让客户方自己装配复杂对象或聚合的话,就会将领域知识泄露到客户方代码中去。
  • 对象的创建本身可以是一个主要操作,但被创建的对象并不适合承担复杂的装配操作。将这些职责混在一起可能产生难以理解的拙劣设计。让客户直接负责创建对象又会使客户的设计陷入混乱,并且破坏被装配对象或聚合的封装,而且导致客户与被创建对象的实现之间产生过于紧密的耦合。

我们在和外部公司进行对接的时候,大多数会通过 https 接口进行远程调用,这时我们接受到返回的参数,很可能是个 json 格式,因为依赖不了外部公司的 api 包,json 就不能反序列化成一个 DTO,这时候 json 转化成可使用的对象就比较复杂,我们可以把复杂的转化工作交给工厂,工厂此时的作用就是把 json 格式转化成可直接使用的对象。这种场景在对接基金保险公司时特别常见,基金保险公司的外部接口往往很复杂,字段非常多,这时候转化工作就很繁琐。

防腐层(ACL)Anti-Corruption Layer: 用于隔离两个系统,允许两个系统之间在不知道对方领域知识的情况下进行集成
失血模型:失血模型是指领域对象里只有get和set方法(POJO),所有的业务逻辑都不包含在内而是放在Business Logic层,大多数MVC架构都使用的失血模型,对象只是数据的载体没有业务行为
贫血模型:领域对象包含大部分业务逻辑,不包含持久化逻辑
充血模型:包含大部分业务逻辑和持久话化逻辑,serivce层很薄仅封装事务和少量逻辑

解释聚合、聚合根、实体、值对象的关系
如果一个客户消亡,客户联系方式,客户的多张银行账户信息将不再有任何意义
客户就是聚合根,银行卡就是实体,联系方式就是值对象,把这些信息组合到一起就是聚合
限界上下文

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值