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

news2024/11/8 9:51:43

说在前面

在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 个模式有助于更好的设计和实现微服务,同时看到业界对微服务的兴趣日渐增长,所以也特意将这些模式记录并发布。

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

微软:微服务设计模式

微软:微服务设计模式

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

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

  • 外交官模式(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)介于新应用和旧应用之间,用于确保新应用的设计不受老应用的限制。是一种在不同应用间转换的机制。

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

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

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

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

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

防腐层不仅仅是一层简单的调用封装,在实际开发中,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的情况下,系统需要直接依赖外部对象和外部调用接口,调用逻辑如下:

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

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

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

COLA框架中的防腐层

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

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

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

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

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

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

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

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

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

回顾COLA分层架构

先来看两张官方介绍图

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

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

其次,还有一个官方的表格,介绍了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层mapperibatis数据库映射
Infra层config配置信息
Client SDKapi服务对外透出的API
Client SDKdto服务对外的DTO

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

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

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

<modules>
  <module>demo-web-client</module>
  <module>demo-web-adapter</module>
  <module>demo-web-app</module>
  <module>demo-web-domain</module>
  <module>demo-web-infrastructure</module>
  <module>start</module>
</modules>

start层

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

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

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

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三端都要支持。

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。

从上图中可以看到,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<CustomerDTO> listByName(CustomerListByNameQry customerListByNameQry) {
        return customerListByNameQryExe.execute(customerListByNameQry);
    }
}

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

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

什么是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 的核心思想是将这两类不同的操作进行分离,可以是两个独立的应用,两个不同的数据源,也可以是同一个应用内的不同接口上。

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

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

但这不是我们现在需要做的,我们是要利用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 ),并且严格按照业务分包

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

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

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

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

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

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

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

domain层

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

可以看到,首先是按照不同的领域(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;
    }
}

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

infrastructure层

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

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

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

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 ,获取用户信息, 两个微服务之间的关系如下:

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

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

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

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

大致的流程如下所示:

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

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

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

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

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

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

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

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

大致的流程如下所示:

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

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

理论上,为了彻底解耦, 我们需要遵循上述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,请到下面公号【技术自由圈】取↓↓↓

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1164759.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CIM与MES

CIM系统&#xff0c;全称计算机集成制造系统&#xff08;Computer-Integrated Manufacturing&#xff09;&#xff0c;是一种集成了计算机技术、网络通讯技术和软件系统的制造自动化框架。CIM的主要目标是整合制造过程中的所有活动&#xff0c;包括生产管理、设备管理和品质管理…

物流小程序制作教程:从零到有,详细解析

随着互联网的快速发展&#xff0c;物流行业也逐渐实现了数字化转型。为了满足消费者对更加便捷、高效的服务需求&#xff0c;许多物流企业选择制作自己的小程序。本文将通过乔拓云网后台&#xff0c;带你轻松搭建物流小程序&#xff0c;主要分为以下几个部分&#xff1a; 一、进…

设置echarts折线图虚线

itemStyle:{normal: {lineStyle: { type: solid}}}itemStyle:{normal: {lineStyle: { type: dashed}}}放到每个红框里面

梯度消失和梯度爆炸的原因

梯度消失和梯度爆炸 梯度爆炸和梯度消失本质上是因为梯度反向传播中的连乘效应。 梯度下降算法 举一个简单的例子,函数表达式为loss 2w^2 4w,如下图 ​​​​​​​ ​​​​​​​ 为了求得w的最优值,使得loss最小,从上图很容易看出来当w -1时,loss最小…

ER图设计神器,帮你省时省力,高效完成工作!

ER图&#xff08;Entity-Relationship Diagram&#xff09;工具用于设计数据库模型&#xff0c;通常用于表示数据实体、关系和属性之间的关系。以下是10个好用的ER图工具。 一、Lucidchart Lucidchart 是一款基于云的协作式图表设计工具&#xff0c;它允许用户创建、编辑和共享…

SAP发票及复制控制

一、概述 众所周知&#xff0c;SAP为业财一体化的ERP管理系统&#xff0c;因此财务发票必不可少&#xff0c;很多外贸企业还会用到形式发票。 发票相关的配置主要包含&#xff1a;发票类型、复制控制、发票定价以及自动过账的配置。 二、系统配置 1. 发票类型 1.1 概述 发…

Latex安装使用教程

在论文投稿时有些期刊要求使用Latex格式&#xff0c;比如博主现在就遇到了这个问题&#xff0c;木有办法&#xff0c;老老实实的学呗。大家可以去官网下载&#xff0c;但官网的界面设计属实有些一言难尽&#xff0c;因此我们可以使用国内的镜像。 LaTeX 基于 TeX&#xff0c;主…

2023年双十一第2波红包活动淘宝天猫京东双11红包领取优惠券跨店满多少减多少规则?

本文为大家提供众多福利&#xff1a; 2023年淘宝/天猫双十一红包第2波活动时间与领取入口&#xff0c;最高23888超级红包 2023年京东双十一红包第2波活动时间与领取入口&#xff0c;最高11111京享红包 草柴APP领淘宝/天猫、京东大额内部隐藏优惠券&#xff0c;拿购物返利 美…

Content-Type 值有哪些?

1、application/x-www-form-urlencoded 最常见 POST 提交数据的方式。 浏览器的原生 form 表单&#xff0c;如果不设置 enctype 属性&#xff0c;那么最终就会以 application/x-www-form-urlencoded 方式提交数据。 <form action"http://www.haha/ads/sds?name小草莓…

根据Aurora发送时序,造Aurora 发送数据包

首先Aurora采用AXIS接口 由于后续需要进行AXIS接口 不同时钟域的数据位宽转换&#xff08;64bit和256bit之间的转换&#xff09;&#xff0c;因此分两次走。 第一种方法&#xff1a;采用AXIS数据位宽转换IP AXIS跨时钟域IP 第二种方法&#xff1a;逻辑完成 下面记录逻辑…

ERP集成WMS仓储管理系统提升仓储效率

随着企业业务规模的不断扩张&#xff0c;仓储管理逐渐凸显其重要性。传统的仓储管理方式&#xff0c;随着业务量的增长&#xff0c;可能带来种种问题&#xff0c;如数据不准确、效率低下、成本增高等。在这样的背景下&#xff0c;ERP集成WMS仓储管理系统的出现成为企业的及时雨…

麒麟KYLINIOS软件仓库搭建01-新创建软件仓库服务器

原文链接&#xff1a;麒麟KYLINIOS软件仓库搭建01-新创建软件仓库服务器 hello&#xff0c;大家好啊&#xff0c;今天给大家带来麒麟桌面操作系统软件仓库搭建的文章01-新创建软件仓库服务器&#xff0c;本篇文章主要给大家介绍了如何在麒麟桌面操作系统2203-x86版本上搭建内网…

Redis高可用解决方案之Redis集群,和Spring Cloud集成实战

专栏集锦&#xff0c;大佬们可以收藏以备不时之需 Spring Cloud实战专栏&#xff1a;https://blog.csdn.net/superdangbo/category_9270827.html Python 实战专栏&#xff1a;https://blog.csdn.net/superdangbo/category_9271194.html Logback 详解专栏&#xff1a;https:/…

Java网站如何集成支付宝当面付,企业个人都能使用的支付(比较简单)

创建应用 这个得先去登录 - 支付宝创建应用 相关配置设置 maven配置 <dependency><groupId>com.alipay.sdk</groupId><artifactId>alipay-sdk-java</artifactId><version>4.38.10.ALL</version></dependency> 支付服务代码 …

Linux:Docker-yum安装(2)

yum在线安装 我这里使用的是centos7默认仓库 如果没有了&#xff0c;可以去下面这个链接下载回来 KALItarro/default-yum: centos7-默认yum仓库 (github.com)https://github.com/KALItarro/default-yum wget -O /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.…

【Git企业开发】第三节.Git的合并冲突

作者简介&#xff1a;大家好&#xff0c;我是未央&#xff1b; 博客首页&#xff1a;未央.303 系列专栏&#xff1a;Git企业级开发 每日一句&#xff1a;人的一生&#xff0c;可以有所作为的时机只有一次&#xff0c;那就是现在&#xff01;&#xff01;&#xff01;&#xff0…

海外问卷调查现在还可以做吗?

可以做&#xff0c;海外问卷调查是一个稳定长期的互联网创业项目。 大家好&#xff0c;我是橙河&#xff0c;这篇文章讲一讲海外问卷调查现在还可以做吗&#xff1f; 海外问卷调查&#xff0c;简单来说&#xff0c;就是外国的商业公司对外发放的付费调查问卷&#xff0c;按照…

1,2,3,4,5 专家正上路

早在20世纪70年代&#xff0c;德雷福斯兄弟(Hubert Dreyfus和Stuart Dreyfus)就开始研究人类如何获取和掌握技能&#xff0c;他们考察了日常生活中常见的各项技能活动&#xff0c;如开车、下棋、体育运动等&#xff0c;提出了德雷福斯模型。它是种构建理论&#xff0c;概括了从…

第十六章 隐马尔科夫模型

文章目录 简介概念随机变量与随机过程马尔可夫链隐含马尔可夫模型两个基本假设三个基本问题 算法观测序列生成算法概率计算算法前向概率与后向概率前向算法后向算法小结 概率与期望 学习问题监督学习方法Baum-Welch算法 预测算法近似算法(MAP)维特比算法(Viterbi) 简介 动态贝叶…

java--this关键字

1.this是什么 this就是一个变量&#xff0c;可以用在方法中&#xff0c;来拿到当前对象(这个this就相当于你的车钥匙&#xff0c;当你在把车停到了车库&#xff0c;然后你发现找不到你的车的时候&#xff0c;就会按一下车钥匙&#xff0c;让你的车叫一下&#xff0c;你就会晓得…