掘金 后端 ( ) • 2024-05-03 18:09

MVC模式, 三层架构, DDD架构, 这是Java开发常用的几种开发模式,本篇笔者会对他们进行简要记录。

MVC模式(Model-View-Controller)

MVC是一种软件设计模式,用于将应用程序分为三个主要部分:模型、视图和控制器。在Java开发中,MVC常用于构建Web应用程序。

  • 模型(Model) :模型代表应用程序的数据和业务逻辑。在Java中,模型通常是POJO(Plain Old Java Object)或JavaBean,负责处理数据持久化、业务逻辑和数据校验等任务。
  • 视图(View) :视图是用户界面的表示,负责展示模型中的数据给用户,并接收用户的输入。在Java中,视图通常是JSP(JavaServer Pages)、Thymeleaf或者HTML等。
  • 控制器(Controller) :控制器是模型和视图之间的协调者,负责处理用户的请求并调用相应的模型来处理业务逻辑,然后选择正确的视图来展示结果。在Java中,控制器通常是基于Servlet或Spring MVC等框架实现的。

三层架构

三层架构将应用程序分为三个主要层次:表示层(Presentation Layer)、业务逻辑层(Business Logic Layer)和数据访问层(Data Access Layer)。

  • 表示层(Presentation Layer) :表示层是用户与应用程序交互的界面,负责接收用户的输入并展示相应的输出。
  • 业务逻辑层(Business Logic Layer) :业务逻辑层包含应用程序的核心业务逻辑,负责处理业务规则、计算和流程控制等任务。
  • 数据访问层(Data Access Layer) :数据访问层负责与数据源(如数据库、文件系统等)进行交互,执行数据的增删改查等操作。

DDD架构

上面两种模式用的很多,简要带过。

关于DDD, 这篇博客讲的很详细https://blog.csdn.net/bookssea/article/details/127248954, 这里笔者尝试用自己的语言记录,加深印象

基础概念

领域

领域是用来确定业务范围的,范围即边界,这也是 DDD 在设计中不断强调边界的原因。简言之,DDD 的领域就是这个边界内要解决的业务问题域。

领域可以进一步划分为子领域。把子领域可以称为子域,每一个子域对应一个更小的业务范围。简单理解就是,遇到复杂问题时,将问题拆分成一个个小问题,再对拆分出来的小问题进行研究,,探索和建立所有子域的知识体系。当所有问题子域完成研究时,我们就建立了全部领域的完整知识体系了。

在领域不断划分的过程中,领域会细分为不同的子域,子域可以根据自身重要性和功能属性划分为三类子域,它们分别是:核心域、通用域和支撑域。

  • 核心域:决定产品和公司核心竞争力的子域,它是业务成功的主要因素和公司的核心竞争力
  • 通用域:没有太多个性化的诉求,同时被多个子域使用的通用功能子域,比如权限认证这种多个子域可以共用。
  • 支撑域:必须的,但既不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域。比如代码类的数据字典。

贫血模型和充血模型

image.png

贫血模型

贫血模型具有一堆属性和set get方法,存在的问题就是通过pojo这个对象上看不出业务有哪些逻辑,一个pojo可能被多个模块调用,只能去上层各种各样的service来调用,这样以后当梳理这个实体有什么业务,只能一层一层去搜service,也就是贫血失忆症,不够面向对象。

在长期维护的 MVC 架构的项目中,你就会发现这里的 DAO、PO、VO 对象,在 Service 层相互调用。那么长期开发后,就导致了各个 PO 里的属性字段数量都被撑的特别大。这样的开发方式,将”状态”“行为“分离到不同的对象中,代码的意图渐渐模糊,膨胀、臃肿和不稳定的架构,让迭代成本增加。

充血模型

将各个属于自己领域范围内的行为和逻辑封装到自己的领域包下处理。这也是 DDD 架构设计的精髓之一。它希望在分治层面合理切割问题空间为更小规模的若干子问题,而问题越小就容易被理解和处理,做到高内聚低耦合。这也是康威定律所提到的,解决复杂场景的设计主要分为:分治、抽象和知识。

比如user用户有改密码,改手机号,修改登录失败次数等操作,都内聚在这个user实体中,每个实体的业务都是清晰的。

@NoArgsConstructor
@Getter
public class User extends Aggregate<Long, User> {

    /**
     * 用户名
     */
    private String userName;

    /**
     * 姓名
     */
    private String realName;

    /**
     * 手机号
     */
    private String phone;

    /**
     * 密码
     */
    private String password;

    /**
     * 锁定结束时间
     */
    private Date lockEndTime;

    /**
     * 登录失败次数
     */
    private Integer failNumber;

    /**
     * 用户角色
     */
    private List<Role> roles;

    /**
     * 部门
     */
    private Department department;

    /**
     * 用户状态
     */
    private UserStatus userStatus;

    /**
     * 用户地址
     */
    private Address address;

    public User(String userName, String phone, String password) {

        saveUserName(userName);
        savePhone(phone);
        savePassword(password);
    }

    /**
     * 保存用户名
     * @param userName
     */
    private void saveUserName(String userName) {
        if (StringUtils.isBlank(userName)){
            Assert.throwException("用户名不能为空!");
        }

        this.userName = userName;
    }

    /**
     * 保存电话
     * @param phone
     */
    private void savePhone(String phone) {
        if (StringUtils.isBlank(phone)){
            Assert.throwException("电话不能为空!");
        }

        this.phone = phone;
    }

    /**
     * 保存密码
     * @param password
     */
    private void savePassword(String password) {
        if (StringUtils.isBlank(password)){
            Assert.throwException("密码不能为空!");
        }

        this.password = password;
    }

    /**
     * 保存用户地址
     * @param province
     * @param city
     * @param region
     */
    public void saveAddress(String province,String city,String region){
        this.address = new Address(province,city,region);
    }

    /**
     * 保存用户角色
     * @param roleList
     */
    public void saveRole(List<Role> roleList) {

        if (CollectionUtils.isEmpty(roles)){
            Assert.throwException("角色不能为空!");
        }

        this.roles = roleList;
    }
}

DDD分层架构

DDD 的分层架构在不断发展。最早是传统的四层架构;再后来领域层和应用层之间增加了上下文环境(Context)层,五层架构(DCI)就此形成了。

用户接口层

用户接口层是前端应用和微服务之间服务访问和数据交换的桥梁。它处理前端发送的Restful 请求和解析用户输入的配置文件等,将数据传递给应用层。或获取应用服务的数据后,进行数据组装,向前端提供数据服务。主要服务形态是 Facade 服务。Facade 服务分为接口和实现两个部分。完成服务定向,DO 与 DTO 数据的转换和组装,实现前端与应用层数据的转换和交换。

  1. 一般包括用户接口、Web 服务、rpc请求,mq消息等外部输入均被视为外部输入的请求。对外暴露API,具体形式不限于RPC、Rest API、消息等。
  2. 一般都很薄,提供必要的参数校验和异常捕获流程。
  3. 一般会提供VO或者DTO到Entity或者ValueObject的转换,用于前后端调用的适配,当然dto可以直接使用command和query,视情况而定。
  4. 用户接口层很重要,在于前后端调用的适配。若你的微服务要面向很多应用或渠道提供服务,而每个渠道的入参出参都不一样,你不太可能开发出太多应用服务,这样Facade接口就起很好的作用了,包括DO和DTO对象的组装和转换等。
应用层

应用层是很薄的一层,理论上不应该有业务规则或逻辑,主要面向用例和流程相关的操作。但应用层又位于领域层之上,因为领域层包含多个聚合,所以它可以协调多个聚合的服务和领域对象完成服务编排和组合,协作完成业务操作。除了同步方法调用外,还可以发布或者订阅领域事件,权限校验、事务控制,一个事务对应一个聚合根。

应用层负责不同聚合之间的服务和数据协调,负责微服务之间的事件发布和订阅。通过应用服务对外暴露微服务的内部功能,这样就可以隐藏领域层核心业务逻辑的复杂性以及内部实现机制。应用层的主要服务形态有:应用服务、事件发布和订阅服务。应用服务内用于组合和编排的服务,主要来源于领域服务,也可以是外部微服务的应用服务。

领域层

领域层包含聚合根、实体、值对象、领域服务等领域模型中的领域对象。

这里我要特别解释一下其中几个领域对象的关系,以便你在设计领域层的时候能更加清楚。首先,领域模型的业务逻辑主要是由实体和领域服务来实现的,其中实体会采用充血模型来实现所有与之相关的业务功能。其次,你要知道,实体和领域对象在实现业务逻辑上不是同级的,当领域中的某些功能,单一实体(或者值对象)不能实现时,领域服务就会出马,它可以组合聚合内的多个实体(或者值对象),实现复杂的业务逻辑。

领域层主要的服务形态有实体方法和领域服务。实体采用充血模型,在实体类内部实现实体相关的所有业务逻辑,实现的形式是实体类中的方法。实体是微服务的原子业务逻辑单元。在设计时我们主要考虑实体自身的属性和业务行为,实现领域模型的核心基础能力。不必过多考虑外部操作和业务流程,这样才能保证领域模型的稳定性。

  1. 包含了业务核心的领域模型:实体(聚合根+值对象),使用充血模型实现所有与之相关的业务功能,主要表达业务概念,业务状态信息以及业务规则。
  2. 真正的业务逻辑都在领域层编写,聚合根负责封装实现业务逻辑,对应用层暴露领域级别的服务接口。
  3. 聚合根不能直接操作其它聚合根,聚合根与聚合根之间只能通过聚合根ID引用;同限界上下文内的聚合之间的领域服务可直接调用;两个限界上下文的交互必须通过应用服务层抽离接口->适配层适配。
  4. 跨实体的状态变化,使用领域服务,领域服务不能直接修改实体的状态,只能调用实体的业务方法

DDD 提倡富领域模型,尽量将业务逻辑归属到实体对象上,实在无法归属的部分则设计成领域服务。领域服务会对多个实体或实体方法进行组装和编排,实现跨多个实体的复杂核心业务逻辑。对于严格分层架构,如果单个实体的方法需要对应用层暴露,则需要通过领域服务封装后才能暴露给应用服务

基础层

也叫基础设施层,基础层是贯穿所有层的,它的作用就是为其它各层提供通用的技术和基础服务,包括第三方工具、驱动、消息中间件、网关、文件、缓存以及数据库等。比较常见的功能还是提供数据库持久化。

基础层的服务形态主要是仓储服务。仓储服务包括接口和实现两部分。仓储接口服务供应用层或者领域层服务调用,仓储实现服务,完成领域对象的持久化或数据初始化。

比如说,在传统架构设计中,由于上层应用对数据库的强耦合,很多公司在架构演进中最担忧的可能就是换数据库了,因为一旦更换数据库,就可能需要重写大部分的代码,这对应用来说是致命的。那采用依赖倒置的设计以后(说白了就是多套一层接口),应用层就可以通过解耦来保持独立的核心业务

  1. 为业务逻辑提供支撑能力,提供通用的技术能力,仓库写增删改查类似DAO。
  2. 防腐层实现(封装变化)用于业务检查和隔离第三方服务,内部try catch
防腐层(ACL)

当某个功能模块需要依赖第三方系统提供的数据或者功能时,我们常用的策略就是直接使用外部系统的API、数据结构。这样存在的问题就是,因使用外部系统,而被外部系统的质量问题影响,从而“腐化”本身设计的问题。

因此我们的解决方案就是在两个系统之间加入一个中间层,隔离第三方系统的依赖,对第三方系统进行通讯转换和语义隔离,这个中间层,我们叫它防腐层

说白了就是,两个系统之间加了中间层,中间层类似适配器模式,解决接口差异的对接,接口转换是单向的(即从调用方向被调用方进行接口转换);防腐层强调两个子系统语义解耦,接口转换是双向的。

image.png

如上图所示的引入了防腐层的架构中:

  • 子系统A与防腐层之间的通讯,使用子系统A的数据模型和架构;
  • 子系统B与防腐层之间的通讯,则使用子系统B的数据模型和架构;
  • 防腐层实现了在两个系统之间进行通讯转换的全部逻辑(双向转换)。