DDD学习笔记4-领域驱动设计如何应对软件复杂度

学习资源来自Gitchat上张逸的《领域驱动设计实践》

不管是因为规模与结构制造的理解力障碍,还是因为变化带来的预测能力问题,最终的决定因素还是因为需求。需求分为业务需求与质量属性需求,因而需求引起的复杂度可以分为两个方面:

  1. 质量属性需求带来的技术复杂度
    诸如安全、高性能、高并发、高可用性等需求,为软件设计带来了极大的挑战,让人痛苦的是这些因素彼此之间可能又互相矛盾、互相影响。例如为了满足系统的高并发访问,我们需要对应用服务进行物理分解,通过横向增加更多的机器来分散访问负载;同时,还可以将一个同步的访问请求拆分为多级步骤的异步请求,再通过引入消息中间件对这些请求进行整合和分散处理。这种分离一方面增加了系统架构的复杂性,另一方面也因为引入了更多的资源,使得系统的高可用面临挑战,并增加了维护数据一致性的难度。

  2. 业务需求带来的业务复杂度
    这种复杂度往往会随着需求规模的增大而增加。由于需求不可能做到完全独立,一旦规模扩大到一定程度,不仅产生了功能数量的增加,还会因为功能互相之间的依赖与影响使得这种复杂度产生叠加,进而影响到整个系统的质量属性,比如系统的可维护性与可扩展性。另外还会因为沟通不畅、客户需求不清晰等多种局外因素而带来的需求变更和修改。如果不能很好地控制这种变更,则可能会因为多次修改而导致业务逻辑纠缠不清,系统可能开始慢慢腐烂而变得不可维护,最终形成一个“大泥球”系统。

领域驱动设计通过分层架构六边形架构来确保业务逻辑与技术实现的隔离。

分层架构

分层架构依照“关注点分离”原则,将属于业务逻辑的关注点放到领域层(Domain Layer)中,而将支撑业务逻辑的技术实现放到基础设施层(Infrastructure Layer)中。同时,领域驱动设计又颇具创见的引入了应用层(Application Layer),应用层扮演了双重角色。一方面它作为业务逻辑的外观(Facade),暴露了能够体现业务用例的应用服务接口;另一方面它又是业务逻辑与技术实现的粘合剂,实现二者之间的协作。

六边形架构

六边形架构则以“内外分离”的方式,更加清晰地勾勒出了业务逻辑与技术实现的边界,且将业务逻辑放在了架构的核心位置。


image.png

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

不了解六边形架构可以看看这篇

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

package practiceddd.ecommerce.ordercontext.application;

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

    public void placeOrder(Identity buyerId, List 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 forBuyerId(Identity buyerId);
    void add(Order order);
} 

public class PlaceOrderService {
    @Repository
    private OrderRepository orderRepository;

    @Service
    private OrderValidator orderValidator;    

    public void execute(Identity buyerId, List 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> cacheClient;

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

package practiceddd.ecommerce.ordercontext.application.cache;

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

package practiceddd.ecommerce.ordercontext.infrastructure.cache;

public class RedisCacheClient implements CacheClient {}

限界上下文的分而治之

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

针对前述案例,我们可以将缓存视为一个独立的子系统,它同样拥有自己的业务逻辑和技术实现,因而也可以为其建立属于缓存领域的分层架构。在架构的宏观视角,这个缓存子系统与订单子系统处于同一个抽象层次。这一概念在领域驱动设计中,被称之为限界上下文(Bounded Context)。

针对庞大而复杂的问题域,限界上下文采用了“分而治之”的思想对问题域进行了分解,有效地控制了问题域的规模,进而控制了整个系统的规模。一旦规模减小,无论业务复杂度还是技术复杂度,都会得到显著的降低,在对领域进行分析以及建模时,也能变得更加容易。限界上下文对整个系统进行了划分,在将一个大系统拆分为一个个小系统后,我们再利用分层架构与六边形架构思想对其进行逻辑分层,以确保业务逻辑与技术实现的隔离,其设计会变得更易于把控,系统的架构也会变得更加清晰。

案例:限界上下文帮助架构的演进

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

系统主要涉及的功能包括:

  • 驻外出差雇员的薪酬与福利
  • 税收计划与合规评审
  • 对税收评审的分配管理
  • 税收策略设计与评审
  • 对驻外出差雇员的税收合规评审
  • 全球的 Visa 服务

主要涉及的用户角色包括:

  • Assignee:驻外出差雇员
  • Admin:税务专员
  • Client:出差雇员的雇主

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


image.png

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

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

采用领域驱动设计,我们将架构的主要关注点放在了“领域”,与客户进行了充分的需求沟通和交流。通过分析已有系统的问题域,结合客户提出的新需求,对整个问题域进行了梳理,并利用限界上下文对问题域进行了分解,获得了如下限界上下文:

  • Account Management:管理用户的身份与配置信息;
  • Calendar Management:管理用户的日程与旅行足迹。

之后,客户希望能改进需求,做到全球范围内的工作指派与管理,目的在于提高公司的运营效率。通过对领域的分析,我们又识别出两个限界上下文。在原有的系统架构中,这两个限界上下文同时处于 Front End 与 Office End 之中,属于重复开发的业务逻辑:

  • Work Record Management:实现工作的分配与任务的跟踪;
  • File Sharing:目的是实现客户与会计师事务所之间的文件交换。

随着我们对领域知识的逐渐深入理解与分析,又随之识别出如下限界上下文:

  • Consent:管理合法的遵守法规的状态;
  • Notification:管理系统与客户之间的交流;
  • Questionnaire:对问卷调查的数据收集。

Questionnaire:对问卷调查的数据收集。
这个领域分析的过程实际上就是通过对领域的分析而引入限界上下文对问题域进行分解,通过降低规模的方式来降低问题域的复杂度;同时,通过为模型确定清晰的边界,使得系统的结构变得更加的清晰,从而保证了领域逻辑的一致性。一旦确定了清晰的领域模型,就能够帮助我们更加容易地发现系统的可重用点与可扩展点,并遵循“高内聚、松耦合”的原则对系统职责进行合理分配,再辅以分层架构以划分逻辑边界,如下图所示:


image.png

我们将识别出来的限界上下文定义为微服务,并对外公开 REST 服务接口。UI Applications 是一个薄薄的展现层,它会调用后端的 RESTful 服务,也使得服务在保证接口不变的前提下能够单独演化。每个服务都是独立的,可以单独部署,因而可以针对服务建立单独的代码库和对应的特性团队(Feature Team)。服务的重用性和可扩展性也有了更好的保障,服务与 UI 之间的集成变得更简单,整个架构会更加清晰。

个人寄语:学到现阶段,用自己的白话做点总结:领域驱动设计的初衷是解决软件复杂度问题,复杂度主要由规模、结构、变化造成,其中变化是无处不在的是整个开发过程中都要注意并寻求一个成本与好处的平衡;识别限界上下文分拆分微服务分而治之可以解决规模问题;分层架构+六边形架构解决拆分以后各自的结构问题,分层的意思就是领域层面向业务,基础设施层面向技术,应用层粘合二者并提供接口。六边形架构主要强调通过接口与适配器的方式恪守对领域与基础设施的隔离。
但是对第一篇描述的领域驱动设计的过程还不是很理解透彻,强调建模的时候建立统一语言,然后识别限界上下午进行拆分我都能理解,但是分层架构和六边形架构怎么会用在战略阶段呢?不应该主要是战术阶段吗?

你可能感兴趣的:(DDD学习笔记4-领域驱动设计如何应对软件复杂度)