阿里面试:让代码不腐烂,DDD是怎么做的?

说在前面

在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:

谈谈你的高并发落地经验?

谈谈你对DDD的理解?

如何保证RPC代码不会腐烂,升级能力强?

最近有小伙伴在字节,又遇到了相关的面试题。小伙伴懵了, 他从来没有用过DDD,面挂了。关于DDD,尼恩之前给大家梳理过一篇很全的文章: 阿里一面:谈一下你对DDD的理解?2W字,帮你实现DDD自由

但是尼恩的文章, 太过理论化,不适合刚入门的人员。所以,尼恩也在不断的为大家找更好的学习资料。

前段时间,尼恩在阿里的技术公众号上看到了一篇文章《殷浩详解DDD:领域层设计规范》 作者是阿里 技术大佬殷浩,非常适合于初学者入门,同时也足够的有深度。

美中不足的是, 殷浩那篇文章的行文风格,对初学者不太友好, 尼恩刚开始看的时候,也比较晦涩。

于是,尼恩在读的过程中,把那些晦涩的内容,给大家用尼恩的语言, 浅化了一下, 这样大家更容易懂。

本着技术学习、技术交流的目的,这里,把尼恩修改过的 《殷浩详解DDD:领域层设计规范》,通过尼恩的公众号《技术自由圈》发布出来。

特别声明,由于没有殷浩同学的联系方式,这里没有找殷浩的授权,

如果殷浩同学或者阿里技术公众号不同意我的修改,不同意我的发布, 我即刻从《技术自由圈》公众号扯下来。

另外, 文章也特别长, 我也特别准备了PDF版本。如果需要尼恩修改过的PDF版本,也可以通过《技术自由圈》公众号找到尼恩来获取。

本文是 《从0到1,带大家精通DDD》系列的第3篇, 第1、2篇的链接地址是:

《阿里DDD大佬:从0到1,带大家精通DDD》

《阿里大佬:DDD 落地两大步骤,以及Repository核心模式》

大家可以先看第1篇、第2篇,再来看第3篇,效果更佳。

另外,尼恩会结合一个工业级的DDD实操项目,在第34章视频《DDD的顶奢面经》中,给大家彻底介绍一下DDD的实操、COLA 框架、DDD的面试题。

文章目录

    • 说在前面
    • 代码就变得腐败不堪,咋整?
    • 9 个微服务设计模式
    • 什么是防腐层
    • Anti-corruption layer使用场景
    • 防腐层的设计与实现
      • 两个上下文相互依赖的简单例子
      • 一个简单的防腐层的设计与实现
      • 一个简单的防腐层的设计小结
    • COLA框架中的防腐层
      • 回顾COLA分层架构
      • start层
      • adapter层
      • cilent层
        • 什么是CQRS (Command 与 Query 分离)
      • app层
      • domain层
      • infrastructure层
      • COLA4.0分层总结
    • COLA框架中的防腐层
      • 对微服务中的远程调用进行防腐烂
      • 设计gateway登录网关,防止rpc腐烂
      • Feign的异常统一处理
    • 未完待续,尼恩说在最后
    • 推荐阅读

代码就变得腐败不堪,咋整?

一份业务代码,尤其是互联网业务代码,都有哪些特点? 大概有这几点:

  • 互联网业务迭代快,工期紧,导致代码结构混乱,几乎没有代码注释和文档
  • 互联网人员变动频繁,很容易接手别人的老项目,新人根本没时间吃透代码结构,紧迫的工期又只能让屎山越堆越大。
  • 多人一起开发,每个人的编码习惯不同,工具类代码各用个的,业务命名也经常冲突,影响效率。
  • 大部分团队几乎没有时间做代码重构,任由代码腐烂。

每当我们新启动一个代码仓库,都是信心满满,结构整洁。但是时间越往后,代码就变得腐败不堪,技术债务越来越庞大。

这种情况有解决方案吗?也是有的:

  1. 小组内定期做代码重构,解决技术债务。
  2. 组内设计完善的应用架构,让代码的腐烂来得慢一些。(当然很难做到完全不腐烂)
  3. 设计尽量简单,让不同层级的开发都能快速看懂并上手开发,而不是在一堆复杂的没人看懂的代码上堆更多的屎山。

尼恩曾经见过一个极端的反面案例

  • 10年多时间, 衍生出 50多个不同的版本, 每个版本80%的功能相同,但是代码各种冲突,没有合并
  • 10年多时间,经历过至少 5次推倒重来, 基本换一个领导,恨不得推导重来一次, 感觉老的版本都是不行,只有自己设计的才好
  • 5次推倒重来,每次都是 风风火火/加班到进ICU, 投入了大量的人力/财力。其实大多是重复投入、重复建设
  • 可谓, 一将不才累死三军,
  • 所以,从项目角度来说,一个优秀的架构,对项目是多么重要;从人才角度来说, 一个优秀的架构师,对一个团队来说是多么的重要

如何让你的代码腐烂的尽可能慢一些,让团队的开发效率尽可能快一些。这里有一个核心的设计模式:放腐层(Anti-corruption layer)模式。

9 个微服务设计模式

2017年,微软 AzureCAT 模式和实践团队在 Azure 架构中心发布了 9 个新的微服务设计模式,文中提到的9 个模式包括:

  • 外交官模式(Ambassador)
  • 放腐层(Anti-corruption layer)
  • 后端服务前端(Backends for Frontends)
  • 舱壁模式(Bulkhead)
  • 网关聚合(Gateway Aggregation)
  • 网关卸载(Gateway Offloading)
  • 网关路由(Gateway Routing)
  • 边车模式(Sidecar)
  • 绞杀者模式(Strangler)。

微软团队在 Azure 架构中心文章,也给出了这些模式解决的问题、方案、使用场景、实现考量等。

微软团队称这 9 个模式有助于更好的设计和实现微服务,同时看到业界对微服务的兴趣日渐增长,所以也特意将这些模式记录并发布。

下图是微软团队建议如何在微服务架构中使用这些模式:

阿里面试:让代码不腐烂,DDD是怎么做的?_第1张图片

微软:微服务设计模式

注意:请点击图像以查看清晰的视图!

这些模式绝大多数也是目前业界比较常用的模式,如:

  • 外交官模式(Ambassador)可以用与语言无关的方式处理常见的客户端连接任务,如监视,日志记录,路由和安全性(如 TLS)。
  • 防腐层(Anti-corruption layer)介于新应用和遗留应用之间,用于确保新应用的设计不受遗留应用的限制。
  • 后端服务前端(Backends for Frontends)为不同类型的客户端(如桌面和移动设备)创建单独的后端服务。这样,单个后端服务就不需要处理各种客户端类型的冲突请求。这种模式可以通过分离客户端特定的关注来帮助保持每个微服务的简单性。
  • 舱壁模式(Bulkhead)隔离了每个工作负载或服务的关键资源,如连接池、内存和 CPU。使用舱壁避免了单个工作负载(或服务)消耗掉所有资源,从而导致其他服务出现故障的场景。这种模式主要是通过防止由一个服务引起的级联故障来增加系统的弹性。
  • 网关聚合(Gateway Aggregation)将对多个单独微服务的请求聚合成单个请求,从而减少消费者和服务之间过多的请求。
  • 边车模式(Sidecar)将应用程序的辅助组件部署为单独的容器或进程以提供隔离和封装。

设计模式是对针对某一问题域的解决方案,它的出现也代表了工程化的可能。

随着微服务在业界的广泛实践,相信这个领域将会走向成熟和稳定。

这里,主要介绍反腐层(Anti-corruption layer)模式

什么是防腐层

**反腐层(Anti-corruption layer)**模式最先由 **Eric Evans 在 Domain-Driven Design(域驱动的设计)**中描述。

在许多情况下,我们的系统需要依赖其他系统,但被依赖的系统可能具有不合理的数据结构、API、协议或技术实现。如果我们强烈依赖外部系统,就会导致我们的系统受到**“腐蚀”**。

在这种情况下,通过引入防腐层,可以有效地隔离外部依赖和内部逻辑,无论外部如何变化,内部代码尽可能保持不变。

反腐层(Anti-corruption layer,简称 ACL)介于新应用和旧应用之间,用于确保新应用的设计不受老应用的限制。是一种在不同应用间转换的机制。

阿里面试:让代码不腐烂,DDD是怎么做的?_第2张图片

注意:请点击图像以查看清晰的视图!

创建一个反腐层,以根据客户端自己的域模型为客户提供功能。

反腐层通过其现有接口与另一个系统进行通信,几乎不需要对其进行任何修改。

反腐层是将一个域映射到另一个域,这样使用第二个域的服务就不必被第一个域的概念“破坏”。因此,反腐层隔离不仅是为了保护你的系统免受异常代码的侵害,还在于分离不同的域并确保它们在将来保持分离。

该层可作为应用程序内的组件或作为独立服务实现。

防腐层不仅仅是一层简单的调用封装,在实际开发中,ACL可以提供更多强大的功能:

  • 适配器

    很多时候外部依赖的数据、接口和协议并不符合内部规范,通过适配器模式,可以将数据转化逻辑封装到ACL内部,降低对业务代码的侵入。

  • 缓存

    对于频繁调用且数据变更不频繁的外部依赖,通过在ACL里嵌入缓存逻辑,能够有效的降低对于外部依赖的请求压力。同时,很多时候缓存逻辑是写在业务代码里的,通过将缓存逻辑嵌入ACL,能够降低业务代码的复杂度。

  • 兜底

    如果外部依赖的稳定性较差,提高系统稳定性的策略之一是通过ACL充当兜底,例如在外部依赖出问题时,返回最近一次成功的缓存或业务兜底数据。这种兜底逻辑通常复杂,如果散布在核心业务代码中,会难以维护。通过集中在ACL中,更容易进行测试和修改。

  • 易于测试

    ACL的接口类能够很容易的实现Mock或Stub,以便于单元测试。

  • 功能开关

    有时候,我们希望在某些场景下启用或禁用某个接口的功能,或者让某个接口返回特定值。

    我们可以在ACL中配置功能开关,而不会影响真实的业务代码。

Anti-corruption layer使用场景

在以下情况下使用此模式:

  • 迁移计划为发生在多个阶段,但是新旧系统之间的集成需要维护

    很多人一看到旧系统就想要赶快替换掉他,但是请不要急著想著去替换旧系统,因为这条路充满困难与失败,而且 旧系统通常反而是系统目前最赚钱的部分。更好的做法是在使用旧系统时包上一层 ACL,让你的开发不受影响,甚至可以一点一滴的替换旧系统的功能,达到即使不影响目前功能下也能开发新功能,达到重构的效果!

  • 两个或更多个子系统具有不同的语义,需要对外部上下文的访问进行一次转义

    例如:对接第三方系統。缴费软件中的收银台系统,需要对接不同的支付方式(支付宝、各个银行、信用卡等),这是就需要收银台系统充当一个Anti-corruption layer,将用户的缴费支付信息,转换成各个三方支付系统需要的数据格式。

  • 如果内部多个组件对外部系统需要访问,那么可以考虑将其放到通用上下文中。

    例如:我们有一个抽奖平台,包含有现金券、折扣券、外卖券、出行券等组件,但他们都需要对接用户信息服务,这时就需要在抽奖平台中,搭建一个Anti-corruption layer,作为抽奖平台对接用户信息的通用适配层。

如果新旧系统之间没有重要的语义差异,则此模式可能不适合。

防腐层的设计与实现

在应用服务中,经常需要调用外部服务接口来实现某些业务功能,这就在代码层面引入了对外部系统的依赖。这里就需要用到防腐层,隔离 外部变化。

两个上下文相互依赖的简单例子

举个例子,有 A 和 B 两个上下文,

其中 B 通过开放主机服务提供对外访问,A 上下文请求 B 上下文的 RPC 接口时,B 将会返回一个模型BView,如果 A 直接在领域模型中引用 B 返回的模型BView,将会早上 A 上下文被污染。

B 上下文对外提供的 RPC 接口:

/**
 * B上下文对外暴露的查询服务,查询
 */
public interface BRpcQueryService{

    Response<BView> query(Query query);
}
public class BView{
    private Integer property1;
    private String property2;
    //省略其他属性以及get/set方法
}

当 A 调用ContextBQueryServicequery方法,将会得到BView这个类。

如果 A 的领域模型直接引用了 BView,将会导致 A 自己的上下文被污染,容易引发很多问题:

  • 类级别的改变:随着 B 上下文的迭代,可能 BView 这个类路径、名称、属性名等都会改变。

    举个例子,B 上下文可能会进行系统重构,重构时会重新发布一个新的 jar 包,要求调用方切换到的新的 jar 包上,这个情况在我实际工作中遇到的不少。如果直接将 BView 引入到本地上下文中,A 将需要进行大量的改动,并且需要大量回归测试才能确保切换无风险。

  • 属性级别的:BView 中的某个属性的类型与 A 上下文中对应的属性类型并不一致,因而使用时必须进行强转;BView 中某个字段的名称与本地上下文某个字段的名称相同,调用时容易引起歧义,例如我在工作中遇到过外部接口返回的模型中有个source字段,本地领域模型中也有一个source字段,但是两者的含义并不一致。

当有多个服务依赖此外部接口时,全部的被依赖服务,都需要迁移和改造,这种的成本将会巨大。

同时,外部依赖的兜底、限流和熔断策略也会受到影响。

在复杂系统中,我们应该尽量避免自己的代码因为外部系统的变化而修改。

那么如何实现对外部系统的隔离呢?答案就是引入防腐层(Anti-Corruption Layer,简称ACL)。

一个简单的防腐层的设计与实现

防腐层的设计和实现并不难,主要注意一下要点:

  • 要点1:防腐层方法返回值必须是本地上下文的值对象或者基本数据类型,不得返回外部上下文的模型

伪代码如下:


public class BContextGateway{

    private BRpcQueryService bRpc;

    public SomeValue queryFromBContext(Prams params){
        //封装查询报文
        Query query=this.fromPrams(params);
        //执行查询
        Response<BView> bResponse=bRpc.query(query);
        //忽略判空、查询失败等逻辑
        BView bView=bResponse.getData();
        //重点:封装本地上下文的值对象进行返回
        return new SomeValue(bView.getProperty1());
    }
}

另外,防腐层方法要捕获外部异常,并抛出的本地上下文自定义的异常, 伪代码如下:


public class BContextGateway{

    private BRpcQueryService bRpc;

    public SomeValue queryFromBContext(Prams params){
        //封转查询报文
        Query query=this.fromPrams(params);
        Response<BView> bResponse;
        try{
            //查询结果
            bResponse=bRpc.query(query);
        }catch(Exception e){
            //重点:捕获异常,并抛出本地自定义的异常
            throw new QueryContextBException();
        }
        //省略其他逻辑
    }
}
  • 要点2:外部上下文返回的错误码,应该转化成本地异常进行抛出,不应该将错误码返回给上层,

    要点2伪代码如下:


public class BContextGateway{

    private BRpcQueryService bRpc;

    public SomeValue queryFromBContext(Prams params){
        //封转查询报文
        Query query=this.fromPrams(params);
        //执行查询
        Response<BView> bResponse=bRpc.query(query);

        //重点:根据错误码时抛出本地自定义的异常
        if("1".equals(bResponse.getCode())){
            throw new QueryContextBException();
        }
        //忽略其他逻辑
    }
}
  • 要点3:按需返回,只返回需要的字段或者数据类型。

    只返回需要的字段,这个很好理解不用过多解释;只返回需要的数据类型。

举个例子,外部上下文可能返回字符串的 0 和 1 代表 false 和 true,但是我们本地是使用布尔类型的,因此要在防腐层转换好再返回。伪代码如下:

public class BContextGateway{

    private BRpcQueryService bRpc;

    public Boolean checkFromBContext(Prams params){
        //封转查询报文
        Query query=this.fromPrams(params);
        //执行查询
        Response<Integer> bResponse=bRpc.check(query);

        //重点:查询失败,根据错误码时抛出本地自定义的异常
        if("ERROR".equals(bResponse.getCode())){
            throw new QueryContextBException();
        }
        //转换成需要的布尔类型进行返回
        return "1".equals(bResponse.getData());
    }
}

这样,经过ACL改造后,ApplicationService的代码已不再直接依赖外部的类和方法,而是依赖我们自己内部定义的值类和接口。

如果未来外部服务发生任何变化,只需修改Facade类和数据转换逻辑,而不需要修改ApplicationService的逻辑。

一个简单的防腐层的设计小结

在没有防腐层ACL的情况下,系统需要直接依赖外部对象和外部调用接口,调用逻辑如下:

阿里面试:让代码不腐烂,DDD是怎么做的?_第3张图片

注意:请点击图像以查看清晰的视图!

而有了防腐层ACL后,系统只需要依赖内部的值类和接口,调用逻辑如下:

阿里面试:让代码不腐烂,DDD是怎么做的?_第4张图片

注意:请点击图像以查看清晰的视图!

COLA框架中的防腐层

**COLA提供了一整套代码架构,拿来即用。**COLA 架构是阿里发布的一套DDD脚手架,是一个整洁的,面向对象的,分层的,可扩展的应用架构,可以帮助降低复杂应用场景的系统熵值,提升系统开发和运维效率。

不管是传统的分层架构、六边形架构、还是洋葱架构,都提倡以业务为核心,解耦外部依赖,分离业务复杂度和技术复杂度等,COLA 架构在此基础上融合了 CQRS、DDD、SOLID 等设计思想,形成一套可落地的应用架构。

COLA中包含了很多架构设计思想,包括讨论度很高的领域驱动设计DDD等。

首先主要谈谈COLA架构,COLA的官方博文中是这么介绍的:

在平时我们的业务开发中,大部分的系统都需要:

  • 接收request,响应response;
  • 做业务逻辑处理,像校验参数,状态流转,业务计算等等;
  • 和外部系统有联动,像数据库,微服务,搜索引擎等;

正是有这样的共性存在,才会有很多普适的架构思想出现,比如分层架构、六边形架构、洋葱圈架构、整洁架构(Clean Architecture)、DDD架构等等。

这些应用架构思想虽然很好,但我们很多同学还是“不讲Co德,明白了很多道理,可还是过不好这一生”。问题就在于缺乏实践和指导。COLA的意义就在于,他不仅是思想,还提供了可落地的实践。应该是为数不多的应用架构层面的开源软件。

尼恩会结合一个工业级的DDD实操项目,在第34章视频中,给大家彻底介绍一个 COLA 框架。

回顾COLA分层架构

先来看两张官方介绍图

阿里面试:让代码不腐烂,DDD是怎么做的?_第5张图片

注意:请点击图像以查看清晰的视图!

阿里面试:让代码不腐烂,DDD是怎么做的?_第6张图片

注意:请点击图像以查看清晰的视图!

其次,还有一个官方的表格,介绍了COLA中每个层的命名和含义:

层次 包名 功能 必选
Adapter层 web 处理页面请求的Controller
Adapter层 wireless 处理无线端的适配
Adapter层 wap 处理wap端的适配
App层 executor 处理request,包括command和query
App层 consumer 处理外部message
App层 scheduler 处理定时任务
Domain层 model 领域模型
Domain层 ability 领域能力,包括DomainService
Domain层 gateway 领域网关,解耦利器
Infra层 gatewayimpl 领域网关实现
Infra层 mapper ibatis数据库映射
Infra层 config 配置信息
Client SDK api 服务对外透出的API
Client SDK dto 服务对外的DTO

这两张图和一个表格已经把整个COLA架构的绝大部分内容展现给了大家,但是一下子这么多信息量可能很难消化。

COLA整个示例架构项目是一个Maven父子结构,那我们就从父模块一个个好好过一遍。

首先父模块的pom.xml包含了如下子模块:

<modules>
  <module>demo-web-clientmodule>
  <module>demo-web-adaptermodule>
  <module>demo-web-appmodule>
  <module>demo-web-domainmodule>
  <module>demo-web-infrastructuremodule>
  <module>startmodule>
modules>

start层

COLA 的start模块作为整个应用的启动模块(通常是一个SpringBoot应用),只承担启动项目和全局相关配置项的存放职责。

COLA 的start模块代码目录如下:

阿里面试:让代码不腐烂,DDD是怎么做的?_第7张图片

将启动独立出来,好处是清晰简洁,也能让新人一眼就看出如何运行项目,以及项目的一些基础依赖。

adapter层

接下来就是 demo-web-adapter模块,这里包括平时我们用的controller层(对于Web应用来说),换汤不换药。

只是在定位上,比 web controller 的 层次更高,包括 web 的接口,还包括 mobile (for APP),wap (for mobile html)等等

为啥不叫 Controller?

Controller这个名字主要是来自于MVC,因为是MVC,所以自带了Web应用的烙印。

然而,随着mobile的兴起,现在很少有应用仅仅只支持Web端,通常的标配是Web,Mobile,WAP三端都要支持。

阿里面试:让代码不腐烂,DDD是怎么做的?_第8张图片

cilent层

有了我们说的“controller”层,接下来有的小伙伴肯定就会想,是不是service层啦。

是,也不是。

传统的MVC应用中,一个service层给controller层调用。

service层分为 service interface + service implement , controller 依赖的是 mvc service interface , mvc service implement 的实例,由 spring 容器完成注入。

所以在COLA中,你的adapter层 (/mvc controller层),调用了client层,client层中就是你服务接口的定义,也就是mvc service interface。

阿里面试:让代码不腐烂,DDD是怎么做的?_第9张图片

从上图中可以看到,client包里有:

  • api文件夹:存放服务接口定义
  • dto文件夹:存放传输实体

注意,这里只是服务接口定义,而不是服务层的具体实现,所以在adapter层中,调用的其实是client层的接口:

@RestController
public class CustomerController {

    @Autowired
    private CustomerServiceI customerService;

    @GetMapping(value = "/customer")
    public MultiResponse<CustomerDTO> listCustomerByName(@RequestParam(required = false) String name){
        CustomerListByNameQry customerListByNameQry = new CustomerListByNameQry();
        customerListByNameQry.setName(name);
        return customerService.listByName(customerListByNameQry);
    }

}

而最终接口的具体实现逻辑( mvc service implement 的实例)放到了app层。

@Service
@CatchAndLog
public class CustomerServiceImpl implements CustomerServiceI {

    @Resource
    private CustomerListByNameQryExe customerListByNameQryExe;

    @Override
    public MultiResponse listByName(CustomerListByNameQry customerListByNameQry) {
        return customerListByNameQryExe.execute(customerListByNameQry);
    }
}

总之, Client层:包含的代码应该是常见的服务接口Facade和DTO数据传输对象,如API、DTO、领域事件、Command和Query对象等等。

一个更加复杂的例子如下:

阿里面试:让代码不腐烂,DDD是怎么做的?_第10张图片

什么是CQRS (Command 与 Query 分离)

CQRS(Command Query Responsibility Segregation)是一种简单的设计模式。

CQRS衍生与CQS,即命令和查询分离,CQS是由Bertrand Meyer所设计。

按照这一设计概念,系统中的方法应该分为两种:改变状态的命令和返回值的查询。‘

Greg young将引入了这个设计概念,并将其应用于对象或者组件当中,这就是今天所要将的CQRS。

CQRS背后的主要思想是应用程序更改对象或组件状态(Command)应该与获取对象或者组件信息(Query)分开。

具体来说:CQRS(Command Query Responsibility Segregation),Command 与 Query 分离的一种模式。

  • Command:命令则是对会引起数据发生变化操作的总称,即新增,更新,删除这些操作,都是命令

  • Query:查询则不会对数据产生变化的操作,只是按照某些条件查找数据

CQRS 的核心思想是将这两类不同的操作进行分离,可以是两个独立的应用,两个不同的数据源,也可以是同一个应用内的不同接口上。

阿里面试:让代码不腐烂,DDD是怎么做的?_第11张图片

注意:请点击图像以查看清晰的视图!

从上图可看出,把数据的变更通过数据同步到另一个库用来查询数据,其实就是数据异构。

但这不是我们现在需要做的,我们是要利用CQRS的思想解决领域驱动中查询功能实现复杂的问题

CQRS 说白了,就是“数据查询”和“业务操作”分离。

在COLA 4.0之前,还有Command Bus和Query Bus 。Command Bus(命令总线):是一种接收命令并将命令传递给命令处理程序的队列。Query Bus(查询总线):是一种查询命令并将查询传递给查询处理程序的队列。

在COLA 4.0中,已经移除了Command Bus和Query Bus的处理,进一步简化了COLA架构。

app层

接着上面说的,我们的app模块作为服务的实现,存放了各个业务的实现类( mvc service implement ),并且严格按照业务分包

这里划重点,是先按照业务分包,再按照功能分包的,为何要这么做,后面还会多说两句,先看图:

阿里面试:让代码不腐烂,DDD是怎么做的?_第12张图片

customer和order分别对应了消费着和订单两个业务子领域。

里面是COLA定义app层下面三种功能:

App层 executor 处理request,包括command和query
App层 consumer 处理外部message
App层 scheduler 处理定时任务

可以看到,消息队列的消费者和定时任务,这类平时我们业务开发经常会遇到的场景,也放在app层。

应用层(Application Layer):主要负责获取输入,组装上下文,参数校验,调用领域层做业务处理,如果需要的话,发送消息通知等。

一个更加复杂的(Application Layer)代码结构,大致如下:

阿里面试:让代码不腐烂,DDD是怎么做的?_第13张图片

domain层

接下来便是domain,也就是领域层,先看一下领域层整体结构:

阿里面试:让代码不腐烂,DDD是怎么做的?_第14张图片

可以看到,首先是按照不同的领域(customer和order)分包,里面则是三种主要的文件类型:

  1. 领域实体:实体模型是充血模型,例如官方示例里的Customer.java如下:
@Data
@Entity
public class Customer{

    private String customerId;
    private String memberId;
    private String globalId;
    private long registeredCapital;
    private String companyName;
    private SourceType sourceType;
    private CompanyType companyType;

    public Customer() {
    }

    public boolean isBigCompany() {
        return registeredCapital > 10000000; //注册资金大于1000万的是大企业
    }

    public boolean isSME() {
        return registeredCapital > 10000 && registeredCapital < 1000000; //注册资金大于10万小于100万的为中小企业
    }

    public void checkConfilict(){
        //Per different biz, the check policy could be different, if so, use ExtensionPoint
        if("ConflictCompanyName".equals(this.companyName)){
            throw new BizException(this.companyName+" has already existed, you can not add it");
        }

    }
}
  1. 领域能力:domainservice文件夹下,是领域对外暴露的服务能力,如上图中的CreditChecker
  2. 领域网关:gateway文件夹下的接口定义,这里的接口你可以粗略的理解成一种SPI,也就是交给infrastructure层去实现的接口。

例如CustomerGateway里定义了接口getByById,要求infrastructure的实现类必须定义如何通过消费者Id获取消费者实体信息,而infrastructure层可以实现任何数据源逻辑,比如,从MySQL获取,从Redis获取,还是从外部API获取等等。

public interface CustomerGateway {
    public Customer getByById(String customerId);
}

在示例代码的CustomerGatewayImpl(位于infrastructure层)中,CustomerDO(数据库实体)经过MyBatis的查询,转换为了Customer领域实体,进行返回。完成了依赖倒置。

@Component
public class CustomerGatewayImpl implements CustomerGateway {
    @Autowired
    private CustomerMapper customerMapper;

    public Customer getByById(String customerId){
      CustomerDO customerDO = customerMapper.getById(customerId);
      //Convert to Customer
      return null;
    }
}

阿里面试:让代码不腐烂,DDD是怎么做的?_第15张图片

注意:请点击图像以查看清晰的视图!

infrastructure层

最后是我们的infrastructure也就是基础设施层,

infrastructure层有我们刚才提到的gatewayimpl网关实现,当然,infrastructure层有MyBatis的mapper等数据源的映射和config配置文件。

Infra层 gatewayimpl 网关实现
Infra层 mapper ibatis数据库映射
Infra层 config 配置信息

阿里面试:让代码不腐烂,DDD是怎么做的?_第16张图片

COLA4.0分层总结

了解了这个6层,COLA4.0很简单明了,然后,用一段官方介绍博客原文来总结COLA的层级

1)适配层(Adapter Layer):

负责对前端展示(web,wireless,wap)的路由和适配,对于传统B/S系统而言,adapter就相当于MVC中的controller;

2)应用层(Application Layer):

主要负责获取输入,组装上下文,参数校验,调用领域层做业务处理,如果需要的话,发送消息通知等。层次是开放的,应用层也可以绕过领域层,直接访问基础实施层;

3)领域层(Domain Layer):

主要是封装了核心业务逻辑,并通过领域服务(Domain Service)和领域对象(Domain Entity)的方法对App层提供业务实体和业务逻辑计算。领域是应用的核心,不依赖任何其他层次;

4)基础实施层(Infrastructure Layer):

主要负责技术细节问题的处理,比如数据库的CRUD、搜索引擎、文件系统、分布式服务的RPC等。

COLA框架中的防腐层

领域防腐的重任也落在Infrastructure Layer (基础实施层),外部依赖需要通过gateway的代理和转义,才能被上面的App层和Domain层使用。

对微服务中的远程调用进行防腐烂

在构建微服务时,我们经常需要跨服务调用,比如 登录的时候,需要调用系统服务以获取用户详细信息。

以下是在微服务中使用OpenFeign实现跨服务调用的过程。

以登录为例,authservice 需要调用 SystemService ,获取用户信息, 两个微服务之间的关系如下:

阿里面试:让代码不腐烂,DDD是怎么做的?_第17张图片

在咱们的工程代码中,调用的核心链路,大致如下:

阿里面试:让代码不腐烂,DDD是怎么做的?_第18张图片

以上链路,是尼恩打断点,通过调用链路,画出来的。

阿里面试:让代码不腐烂,DDD是怎么做的?_第19张图片

设计gateway登录网关,防止rpc腐烂

在mvc 架构中, 登录服务 LoginDomainServiceImpl ,如果需要通过rpc获取用户信息, 直接注入 Feign的代理客户端stub 对象, 完成RPC远程调用就可以了。

大致的流程如下所示:

阿里面试:让代码不腐烂,DDD是怎么做的?_第20张图片

这样就会导致 LoginDomainServiceImpl 领域服务,两个问题:

(1)对 Feign RPC 框架进行强依赖,使得 领域服务 LoginDomainServiceImpl 的代码不利于复用和扩展。

LoginDomainServiceImpl 的代码不是纯业务的, 如果要拿到其他地方复用,发现和现有的 cache、rpc等技术组件强耦合。

(2)也不利于底层技术组件的换代和升级

在尼恩的视频中,介绍过使用Dubbo 替代Feign 进行性能调优的实操, 进行性能10倍以上的调优。

但是,这样就需要去修改 领域服务LoginDomainServiceImpl 的代码, 可能会修改引入, 带来一些潜在问题, 带来很多的不确定性。

两种现象,我们这里统称为RPC腐烂。

解决的措施是: 设计gateway登录网关,封装 RPC组件 , 隔离特定的RPC 框架, 防止rpc腐烂。

大致的流程如下所示:

阿里面试:让代码不腐烂,DDD是怎么做的?_第21张图片

在代码维度, 是 服务层依赖 领域 网关

阿里面试:让代码不腐烂,DDD是怎么做的?_第22张图片

在代码维度, 领域 网关 依赖 Feign 组件

阿里面试:让代码不腐烂,DDD是怎么做的?_第23张图片

理论上,为了彻底解耦, 我们需要遵循上述ACL的实现逻辑,gateway 内部 进行 数据的转换。

然而,在实际开发中,由于是内部系统,差异性不太明显,通常可以直接使用OpenFeign进行远程调用,忽略Facade定义和内部类转换的过程。

Feign的异常统一处理

在使用OpenFeign进行远程调用时,如果HTTP状态码为非200,OpenFeign会触发异常解析并进入默认的异常解码器feign.codec.ErrorDecoder,将业务异常包装成FeignException

此时,如果不做任何处理,调用时可以返回的消息会变成FeignException的消息体,如下所示:

显然,这个包装后的异常我们不需要,应该直接将捕获到的生产者的业务异常抛给前端。

那么,如何解决这个问题呢?

可以通过重写OpenFeign的默认异常解码器来实现,代码如下:

@Slf4j
public class FeignClientErrorDecoder implements ErrorDecoder {
    @Override
    public Exception decode(String s, Response response) {
        log.error("捕获到fegin服务端内部异常");
        if(response.status() != HttpStatus.OK.value()){
            if(response.status() == HttpStatus.SERVICE_UNAVAILABLE.value()){
                String errContent;
                GlobalException exception = new GlobalException("内部请求异常");
                try {
                    errContent = Util.toString(response.body().asReader());
                    if(!StringUtils.isEmpty(errContent)){
                        errContent = errContent.replaceAll("\t","").replaceAll("\n","");
                        JSONObject errResp = JSONObject.parseObject(errContent);
                        String errMessage = errResp.getString("message");
                        exception = new GlobalException(errMessage);
                    }
                }catch (Exception e){
                    log.error("feign处理异常错误",e);
                }
                return exception;
            }
        }
        return new GlobalException("未知错误!");
    }
}

此异常解码器直接将异常转化为自定义的GlobalException,表示远程调用异常。

当然,还需要在配置类中注入此异常解码器。

@Slf4j
@Configuration
public class FeignConfig {

    @Bean
    public Request.Options options(){
        return new Request.Options(5000,10000);
    }

    @Bean
    public Logger.Level feignLoggerLevel(){
        return Logger.Level.FULL;
    }

    @Bean
    public ErrorDecoder errorDecoder() {
        return new FeignClientErrorDecoder();
    }

    @Bean
    public FeignAuthInterceptor feignAuthRequestInterceptor(){
        return new FeignAuthInterceptor();
    }
}

当然,可能有许多模块都需要远程调用,我们可以将上述内容构建成一个通用的Starter模块,以便其他业务模块共享。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.xunheng.feign.config.FeignConfig,\
  com.xunheng.feign.config.FeignClientErrorDecoder,\
  com.xunheng.feign.interceptor.FeignAuthInterceptor

未完待续,尼恩说在最后

DDD 面试题,是非常常见的面试题。

DDD的学习材料, 汗牛塞屋,又缺乏经典。

《殷浩详解DDD:领域层设计规范》做到从0到1带大家精通DDD,非常难得。

这里,把尼恩修改过的 《殷浩详解DDD:领域层设计规范》,通过尼恩的公众号《技术自由圈》发布出来。

大家面试的时候, 可以参考以上的内容去组织答案,如果大家能做到对答如流,如数家珍,基本上 面试官会被你 震惊到、吸引到。

另外在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,并且在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。

最终,让面试官爱到 “不能自已、口水直流”。offer, 也就来了。

当然,关于DDD,尼恩即将给大家发布一波视频 《第34章:DDD的顶奢面经》。

推荐阅读

《百亿级访问量,如何做缓存架构设计》

《多级缓存 架构设计》

《消息推送 架构设计》

《阿里2面:你们部署多少节点?1000W并发,当如何部署?》

《美团2面:5个9高可用99.999%,如何实现?》

《网易一面:单节点2000Wtps,Kafka怎么做的?》

《字节一面:事务补偿和事务重试,关系是什么?》

《网易一面:25Wqps高吞吐写Mysql,100W数据4秒写完,如何实现?》

《亿级短视频,如何架构?》

《炸裂,靠“吹牛”过京东一面,月薪40K》

《太猛了,靠“吹牛”过顺丰一面,月薪30K》

《炸裂了…京东一面索命40问,过了就50W+》

《问麻了…阿里一面索命27问,过了就60W+》

《百度狂问3小时,大厂offer到手,小伙真狠!》

《饿了么太狠:面个高级Java,抖这多硬活、狠活》

《字节狂问一小时,小伙offer到手,太狠了!》

《收个滴滴Offer:从小伙三面经历,看看需要学点啥?》

《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓

你可能感兴趣的:(技术圣经,面试,面试,java,架构,数据库,中间件,后端)