架构演化

分层架构演化

领域合并

微服务架构的演进

域模型中对象的层次从内到外依次是:值对象、实体、聚合和限界上下文。实体或值对象的简单变更,一般不会让领域模型和微服务发生大的变化。但聚合的重组或拆分却可以。这是因为聚合内业务功能内聚,能独立完成特定的业务逻辑。那聚合的重组或拆分,势必就会引起业务模块和系统功能的变化了。这里我们可以以聚合为基础单元,完成领域模型和微服务架构的演进。聚合可以作为一个整体,在不同的领域模型之间重组或者拆分,或者直接将一个聚合独立为微服务。

微服务演化

我们结合上图,以微服务 1 为例,讲解下微服务架构的演进过程:

  • 当你发现微服务 1 中聚合 a 的功能经常被高频访问,以致拖累整个微服务 1 的性能时,我们可以把聚合 a 的代码,从微服务 1 中剥离出来,独立为微服务 2。这样微服务 2 就可轻松应对高性能场景。
  • 在业务发展到一定程度以后,你会发现微服务 3 的领域模型有了变化,聚合 d 会更适合放到微服务 1 的领域模型中。这时你就可以将聚合 d 的代码整体搬迁到微服务 1 中。如果你在设计时已经定义好了聚合之间的代码边界,这个过程不会太复杂,也不会花太多时间。
  • 最后我们发现,在经历模型和架构演进后,微服务 1 已经从最初包含聚合 a、b、c,演进为包含聚合 b、c、d 的新领域模型和微服务了。

微服务内服务的演进

在微服务内部,实体的方法被领域服务组合和封装,领域服务又被应用服务组合和封装。在服务逐层组合和封装的过程中,你会发现这样一个有趣的现象。

应用层内合并

我们看下上面这张图。在服务设计时,你并不一定能完整预测有哪些下层服务会被多少个上层服务组装,因此领域层通常只提供一些原子服务,比如领域服务 a、b、c。但随着系统功能增强和外部接入越来越多,应用服务会不断丰富。有一天你会发现领域服务 b 和 c 同时多次被多个应用服务调用了,执行顺序也基本一致。这时你可以考虑将 b 和 c 合并,再将应用服务中 b、c 的功能下沉到领域层,演进为新的领域服务(b+c)。这样既减少了服务的数量,也减轻了上层服务组合和编排的复杂度。

渐进式能力下沉

在现实业务中,很多的功能都是用例特有的(Use case specific)的,如果盲目的使用 Domain 收拢业务并不见得能带来多大的益处。相反,这种收拢会导致 Domain 层的膨胀过厚,不够纯粹,反而会影响复用性和表达能力。所谓的能力下沉,是指我们不强求一次就能设计出 Domain 的能力,也不需要强制要求把所有的业务功能都放到 Domain 层,而是采用实用主义的态度,即只对那些需要在多个场景中需要被复用的能力进行抽象下沉,而不需要复用的,就暂时放在 App 层的 Use Case 里就好了。这里的 Use Case 是《架构整洁之道》里面的术语,简单理解就是响应一个 Request 的处理过程。

这种循序渐进的能力下沉策略,应该是一种更符合实际、更敏捷的方法。因为我们承认模型不是一次性设计出来的,而是迭代演化出来的。下沉的过程如下图所示,假设两个 Use Case 中,我们发现 uc1 的 step3 和 uc2 的 step1 有类似的功能,我们就可以考虑让其下沉到 Domain 层,从而增加代码的复用性。

应用层能力下沉示意图

指导下沉有两个关键指标:代码的复用性和内聚性。复用性是告诉我们 When(什么时候该下沉了),即有重复代码的时候。内聚性是告诉我们 How(要下沉到哪里),功能有没有内聚到恰当的实体上,有没有放到合适的层次上;因为 Domain 层的能力也是有两个层次的,一个是 Domain Service 这是相对比较粗的粒度,另一个是 Domain 的 Model 这个是最细粒度的复用。

比如,在我们的商品域,经常需要判断一个商品是不是最小单位,是不是中包商品。像这种能力就非常有必要直接挂载在 Model 上。

public class CSPU {
  private String code;
  private String baseCode;

  //省略其它属性
  /**
   * 单品是否为最小单位。
   *
   */
  public boolean isMinimumUnit() {
    return StringUtils.equals(code, baseCode);
  }

  /**
   * 针对中包的特殊处理
   *
   */
  public boolean isMidPackage() {
    return StringUtils.equals(code, midPackageCode);
  }
}

之前,因为老系统中没有领域模型,没有 CSPU 这个实体。你会发现像判断单品是否为最小单位的逻辑是以 StringUtils.equals(code, baseCode)的形式散落在代码的各个角落。这种代码的可理解性是可想而知的,至少我在第一眼看到这个代码的时候,是完全不知道什么意思。

上一页
下一页