领域驱动设计 | 对软件复杂度的应对学习笔记(一)

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Evan_Leung/article/details/81639794

领域驱动设计 | 对软件复杂度的应对学习笔记(一)

前言

本文主要基于张逸老师领域驱动设计教程1-7章与本人学习体会整理,后续会持续整理领域驱动设计相关内容,因时间关系,部分内容可能会出现疏忽,望各位高手指正,谢谢!


什么是领域驱动设计

领域驱动设计(Domain Driven Design,DDD)是由 Eric Evans 最早提出的综合软件系统分析和设计的面向对象建模方法,如今已经发展成为了一种针对大型复杂系统的领域建模与分析方法。它完全改变了传统软件开发工程师针对数据库进行的建模方法,从而将要解决的业务概念和业务规则转换为软件系统中的类型以及类型的属性与行为,通过合理运用面向对象的封装、继承和多态等设计要素,降低或隐藏整个系统的业务复杂性,并使得系统具有更好的扩展性,应对纷繁多变的现实业务问题。

领域驱动设计是针对软件开发领域提出的一套系统与理论分析方法,它建立了以领域为核心驱动力的设计体系,因而具有一定的开放性。在这个体系中,你可以使用不限于领域驱动设计提出的任何一种方法来解决这些问题。例如,可以使用用例(Use Case)、测试驱动开发(TDD)、用户故事(User Story)来帮助我们对领域建立模型;可以引入整洁架构思想及六边形架构,以帮助我们建立一个层次分明、结构清晰的系统架构;还可以引入函数式编程思想,利用纯函数与抽象代数结构的不变性以及函数的组合性来表达领域模型。这些实践方法与模型已经超越了 Eric Evans 最初提出的领域驱动设计范畴,但在体系上却是一脉相承的。这也是为什么在领域驱动设计社区,能够不断诞生新的概念诸如 CQRS 模式、事件溯源(Event Sourcing)模式与事件风暴(Event Storming);领域驱动设计也以开放的心态拥抱微服务(Micro Service),甚至能够将它的设计思想与原则运用到微服务架构设计中。


为什么要用领域驱动设计

Eric Evans 在创造性地提出领域驱动设计时,实则是针对当时项目中聚焦在以数据以及数据样式为核心的系统建模方法批判。面向数据的建模方法是关系数据库理论的延续,关注的是数据表以及数据表之间关系的设计。这是典型的面向技术实现的建模方法,面对日渐复杂的业务逻辑,这种设计方法欠缺灵活性与可扩展性,也无法更好地利用面向对象设计思想及设计模式,建立可重用的、可扩展的代码单元。领域驱动设计的提出,是设计观念的转变,蕴含了全新的设计思想、设计原则与设计过程。
领域驱动设计主要是应用在复杂系统——在某种程度上可以预测,但会有很多出乎意料的事情发生。


领域驱动设计过程

这里写图片描述


怎么利用领域驱动设计思想去解决复杂系统

领域驱动设计用以表示模型的主要要素

领域驱动设计用以表示模型的主要要素包括:

  • 值对象(Value Object)
  • 实体(Entity)
  • 领域服务(Domain Service)
  • 领域事件(Domain Event)
  • 资源库(Repository)
  • 工厂(Factory)
  • 聚合(Aggregate)
  • 应用服务(Application Service)
    这里写图片描述

领域驱动设计围绕着领域模型进行设计,通过分层架构(Layered Architecture)将领域独立出来。表示领域模型的对象包括:实体、值对象领域服务领域逻辑都应该封装在这些对象中。这一严格的设计原则可以避免业务逻辑渗透到领域层之外,导致技术实现与业务逻辑的混淆。在领域驱动设计的演进中,又引入了领域事件来丰富领域模型。

聚合是一种边界,它可以封装一到多个实体值对象,并维持该边界范围之内的业务完整性。在聚合中,至少包含一个实体,且只有实体才能作为聚合根(Aggregate Root)。注意,在领域驱动设计中,没有任何一个类是单独的聚合,因为聚合代表的是边界概念,而非领域概念。在极端情况下,一个聚合可能有且只有一个实体。

工厂资源库都是对领域对象生命周期的管理。前者负责领域对象的创建,往往用于封装复杂或者可能变化的创建逻辑;后者则负责从存放资源的位置(数据库、内存或者其他 Web 资源)获取、添加、删除或者修改领域对象。领域模型中的资源库不应该暴露访问领域对象的技术实现细节


领域驱动设计的战略设计阶段是从下面两个方面来考量的

  • 问题域方面:针对问题域,引入限界上下文(Bounded Context)上下文映射(Context Map)对问题域进行合理的分解,识别出核心领域(Core Domain)子领域(SubDomain),并确定领域的边界以及它们之间的关系,维持模型的完整性。
  • 架构方面:通过分层架构来隔离关注点,尤其是将领域实现独立出来,能够更利于领域模型的单一性与稳定性;引入六边形架构可以清晰地表达领域与技术基础设施的边界;CQRS 模式则分离了查询场景和命令场景,针对不同场景选择使用同步或异步操作,来提高架构的低延迟性与高并发能力

利用分层架构六边形架构思想进行逻辑分层

基本思路

  • 限界上下文——通过 限界上下文 对整个系统进行划分,将庞大的软件系统划分为松散耦合的多个小系统,基于业务进行垂直切割
  • 利用 分层架构六边形架构 思想——利用 分层架构六边形架构 思想对其进行逻辑分层,以确保业务逻辑与技术实现的隔离

需求引起的软件复杂度

  • 技术复杂度
    • 技术复杂度来自需求的质量属性——诸如安全、高性能、高并发、高可用性等需求,为软件设计带来了极大的挑战,让人痛苦的是这些因素彼此之间可能又互相矛盾、互相影响。
  • 业务复杂度
    • 业务复杂度对应了客户的业务需求——因而这种复杂度往往会随着需求规模的增大而增加。

技术复杂度与业务复杂度并非完全独立,二者混合在一起产生的化合作用更让系统的复杂度变得不可预期,难以掌控
这里写图片描述


面临问题

  • 问题域过于庞大而复杂,使得从问题域中寻求解决方案的挑战增加,该问题 与软件系统的规模有关
  • 开发人员将业务逻辑的复杂度与技术实现的复杂度混淆在一起,该问题 与软件系统的结构有关
  • 随着需求的增长和变化,无法控制业务复杂度和技术复杂度,该问题 与软件系统的变化有关**

怎么应对?

隔离业务复杂度与技术复杂度

要避免业务逻辑的复杂度与技术实现的复杂度混淆在一起,首要任务就是确定业务逻辑与技术实现的边界,从而隔离各自的复杂度。在理想状态下,我们应该保证业务规则与技术实现是正交的

确保业务逻辑与技术实现的隔离的措施?

  • 分层架构

    • 关注点分离——
      分层架构遵循了“关注点分离”原则,将属于业务逻辑的关注点放到领域层(Domain Layer)中,而将支撑业务逻辑的技术实现放到基础设施层(Infrastructure Layer)中。同时,领域驱动设计又颇具创见的引入了应用层(Application Layer),应用层扮演了双重角色。一方面它作为业务逻辑的外观(Facade),暴露了能够体现业务用例的应用服务接口;另一方面它又是业务逻辑与技术实现的粘合剂,实现二者之间的协作
      • 分层架构——
        下图展现的就是一个典型的领域驱动设计分层架构,蓝色区域的内容与业务逻辑有关,灰色区域的内容与技术实现有关,二者泾渭分明,然后汇合在应用层。应用层确定了业务逻辑与技术实现的边界,通过直接依赖或者依赖注入(DI,Dependency Injection)的方式将二者结合起来:
        这里写图片描述
  • 六边形架构

    • 内外分离——清晰地勾勒出了业务逻辑与技术实现的边界,且将业务逻辑放在了架构的核心位置。
      • 体现业务逻辑的应用层与领域层处于六边形架构的内核,并通过内部的六边形边界与基础设施的模块隔离开。当我们在进行软件开发时,只要恪守架构上的六边形边界,则不会让技术实现的复杂度污染到业务逻辑,保证了领域的整洁。边界还隔离了变化产生的影响。如果我们在领域层或应用层抽象了技术实现的接口,再通过依赖注入将控制的方向倒转,业务内核就会变得更加的稳定,不会因为技术选型或其他决策的变化而导致领域代码的修改。
        这里写图片描述

领域驱动框架分层Demo

结合领域驱动设计分层架构思想,基于DDDSample的Demo模块分层示例:
这里写图片描述


实战案例——隔离数据库与缓存的访问

以电商模型为例,具体实现思路大概如下:

领域驱动设计建议我们在领域层建立资源库(Repository)的抽象,它的实现则被放在基础设施层,然后采用依赖注入在运行时为业务逻辑注入具体的资源库实现。那么,对于处于内核之外的 Repositories 模块而言,即使选择从 MyBatis 迁移到 Sprint Data,领域代码都不会受到牵连:
这里写图片描述

package practiceddd.ecommerce.ordercontext.application;

@Transaction
public class OrderAppService {
    @Service
    private PlaceOrderService placeOrder;

    public void placeOrder(Identity buyerId, List<OrderItem> items, ShippingAddress shipping, BillingAddress billing) {
        try {
            palceOrder.execute(buyerId, items, shipping, billing);
        } catch (OrderRepositoryException | InvalidOrderException | Exception ex) {
            ex.printStackTrace();
            logger.error(ex.getMessage());
        }
    }
}

package practiceddd.ecommerce.ordercontext.domain;

public interface OrderRepository {
    List<Order> forBuyerId(Identity buyerId);
    void add(Order order);
} 

public class PlaceOrderService {
    @Repository
    private OrderRepository orderRepository;

    @Service
    private OrderValidator orderValidator;    

    public void execute(Identity buyerId, List<OrderItem> items, ShippingAddress shipping, BillingAddress billing) {
        Order order = Order.create(buyerId, items, shipping, billing);
        if (orderValidator.isValid(order)) {
            orderRepository.add(order);
        } else {
            throw new InvalidOrderException(String.format("the order which placed by buyer with %s is invalid.", buyerId));
        }
    }
}

package practiceddd.ecommerce.ordercontext.infrastructure.db;

public class OrderMybatisRepository implements OrderRepository {}
public class OrderSprintDataRepository implements OrderRepository {}

对缓存的处理可以如法炮制,但它与资源库稍有不同之处。资源库作为访问领域模型对象的入口,其本身提供的增删改查功能,在抽象层面上是对领域资源的访问。因此在领域驱动设计中,我们通常将资源库的抽象归属到领域层。对缓存的访问则不相同,它的逻辑就是对 key 和 value 的操作与具体的领域无关。倘若要为缓存的访问方法定义抽象接口,在分层的归属上应该属于应用层,至于实现属于技术范畴,应该放在基础设施层
这里写图片描述

package practiceddd.ecommerce.ordercontext.application;

@Transaction
public class OrderAppService {
    @Repository
    private OrderRepository orderRepository;

    @Service
    private CacheClient<List<Order>> cacheClient;

    public List<Order> findBy(Identity buyerId) {
        Optional<List<Order>> cachedOrders = cacheClient.get(buyerId.value());
        if (cachedOrders.isPresent()) {
            return orders.get();
        } 
        List<Order> orders = orderRepository.forBuyerId(buyerId);
        if (!orders.isEmpty()) {
            cacheClient.put(buyerId.value(), orders);
        }
        return orders;
    }
}

package practiceddd.ecommerce.ordercontext.application.cache;

public interface CacheClient<T> {
    Optional<T> get(String key);
    void put(String key, T value);
}

package practiceddd.ecommerce.ordercontext.infrastructure.cache;

public class RedisCacheClient<T> implements CacheClient<T> {}

通过限界上下文对系统垂直方向划分

缓存接口放在了系统的应用层,从层次的职责来看,这样的设计是合理的,但它却使得系统的应用层变得更加臃肿,职责也变得不够单一了。这是分层架构与六边形架构的局限所在,因为这两种架构模式仅仅体现了软件系统的逻辑划分。倘若我们将一个软件系统视为一个纵横交错的魔方,前述的逻辑划分仅仅是一种水平方向的划分;至于垂直方向的划分,则是面向垂直业务的切割。这种方式更利于控制软件系统的规模,将一个庞大的软件系统划分为松散耦合的多个小系统的组合。


什么是领域模型

领域模型是对业务需求的一种抽象,其表达了领域概念、领域规则以及领域概念之间的关系。一个好的领域模型是对统一语言的可视化表示,通过它可以减少需求沟通可能出现的歧义;通过提炼领域知识,并运用抽象的领域模型去表达,就可以达到对领域逻辑的化繁为简。模型是封装,实现了对业务细节的隐藏;模型是抽象,提取了领域知识共同特征,保留了面对变化时能够良好扩展的可能性。


怎样利用限界上下文进行划分

基于领域模型对系统进行垂直划分,例如上述例子中的缓存可以视为一个独立的子系统,它同样拥有自己的业务逻辑和技术实现,因而也可以为其建立属于缓存领域的分层架构。在架构的宏观视角,这个缓存子系统与订单子系统处于同一个抽象层次。这一概念在领域驱动设计中,被称之为限界上下文(Bounded Context)。
这里写图片描述


利用限界上下文对国际报税系统改造

案例背景

国际报税系统是为跨国公司的驻外出差雇员(系统中被称之为 Assignee)提供方便一体化的税收信息填报平台。客户是一家会计师事务所,该事务所的专员(Admin)通过该平台可以收集雇员提交的报税信息,然后对这些信息进行税务评审。如果 Admin 评审出信息有问题,则返回给 Assignee 重新修改和填报。一旦信息确认无误,则进行税收分析和计算,并获得最终的税务报告提交给当地政府以及雇员本人。


系统问题

在早期的架构设计时,架构师并没有对整个系统的问题域进行拆分,而是基于用户角色对系统进行了简单粗暴的划分,分为了两个相对独立的子系统:Frond End 与 Office End,这两个子系统单独部署,分别面向 Assignee 与 Admin。系统之间的集成则通过消息和 Web Service 进行通信。两个子系统的开发分属不同的团队,Frond End 由美国的团队负责开发与维护,而 Office End 则由印度的团队负责。整个架构如下图所示:
这里写图片描述

采用这种架构面临的问题如下:

  • 庞大的代码库:整个 Front End 和 Office End 都没有做物理分解,随着需求的增多,代码库会变得格外庞大。
  • 分散的逻辑:系统分解的边界是不合理的,没有按照业务分解,而是按照用户的角色进行分解,因而导致大量相似的逻辑分散在两个不同的子系统中。
  • 重复的数据:两个子系统中存在业务重叠,因而也导致了部分数据的重复。
  • 复杂的集成:Front End 与 Office End 因为某些相关的业务需要彼此通信,这种集成关系是双向的,且由两个不同的团队开发,导致集成的接口混乱,消息协议多样化。
  • 知识未形成共享:两个团队完全独立开发,没有掌握端对端的整体流程,团队之间没有形成知识的共享。
  • 无法应对需求变化:新增需求包括对国际旅游、Visa 的支持,现有系统的架构无法很好地支持这些变化。

改造流程


这里写图片描述

展开阅读全文

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