知识卡片

跨边界调用的两种依赖方向:低层调高层对齐,高层调低层需反转

结构图卡

内容

跨边界调用指运行时边界一侧的函数调用另一侧的函数并传递数据,构造合理的跨边界调用需要管控源码依赖关系——这正是[[边界的作用是推迟不成熟决策最耗人力的是过早决策导致的耦合]]所说的”针对变更的防火墙”:一个模块源码变更会不会牵连其他模块重新编译部署,取决于这层依赖关系如何管控。最简单的情形是低层客户端调用高层服务函数:Client调用Service上的函数并传入一个数据结构,此时运行时的控制流方向与编译时的源码依赖方向是一致的,都从低层组件指向高层组件,数据结构定义在被调用方(高层)一侧。但当高层组件里的客户端需要调用低层组件的服务时,就必须运用动态多态来反转依赖——控制流跨越边界的方向不变(仍是从调用方指向被调用方),但源码依赖关系被接口反转成了从低层指向高层:高层Client通过Service接口调用低层ServiceImpl的函数,运行时依赖(Client实际用到ServiceImpl的实现)与编译时依赖(ServiceImpl依赖Service接口,而非反过来)正好相反,此时数据结构定义在调用方(高层)一侧。即便是在单体部署、静态链接成一个可执行文件的系统里,这种自律式的组件划分依然极大地帮助了开发测试部署——不同团队能独立开发不同组件、互不干扰,高层组件与低层细节能被良好隔离、独立演进;这也是为什么面向对象编程(提供了安全的动态多态手段)几十年来逐渐成为主流范式——没有它,架构师只能退回到危险的函数指针,多数人权衡利弊后干脆放弃组件划分。

结构图

flowchart LR
    subgraph 情形一: 低层调高层,方向对齐
    Client1[低层Client] -->|运行时调用+编译时依赖,方向一致| Service1["Service.f(Data)<br/>Data定义在Service一侧"]
    end
    subgraph 情形二: 高层调低层,需动态多态反转
    Client2[高层Client] -->|运行时控制流| ServiceImpl[低层ServiceImpl.f]
    ServiceImpl -.编译时依赖: 实现接口,方向与控制流相反.-> Interface["Service接口<br/>Data定义在Client一侧"]
    Client2 -.编译时依赖.-> Interface
    end

参考来源

- 位置:《架构整洁之道》第18章《边界剖析》"跨边界调用""令人生畏的单体结构"(源文件:_epub-src/text/part0014_split_002.html) - 结论依据:原文区分低层调高层(运行时依赖与编译时依赖方向一致)与高层调低层(通过接口反转依赖,运行时依赖与编译时依赖方向相反)两种跨边界调用形式,并说明即使在单体结构里这种依赖管控依然极具意义,直接支撑本卡片的结构图与解释。 - 原始内容:最简单的跨边界调用形式,是由低层客户端来调用高层服务函数,这种依赖关系在运行时和编译时会保持指向一致……当高层组件中的客户端需要调用低层组件中的服务时,我们就需要运用动态形式的多态来反转依赖关系了。