模块化设计:拆出好架构,守住边界线

做SDK这几年,我见过太多代码从“微型单体”长成“巨型泥团”了。一开始就一两个接口,写起来很爽。等版本迭代到第五六个,新功能加不进去,旧逻辑一改就崩。这个时候回头搞模块化?成本已经下不来了。嗯,今天我们就聊聊模块化设计这件事。

一句话总结:模块化不是把代码分文件夹,而是从结构上做好边界控制。

模块拆分:从“按文件”到“按业务”

很多人问过我:“模块到底该怎么拆?按文件类型?按功能?” 我刚开始也犯过这个错。那时候我把网络请求、数据解析、UI渲染全扔在不同文件夹里。看起来挺规整,实际上每次加功能都改三个不同目录的代码。

后来我想明白了。拆模块,核心是看“业务边界”。

举个例子。一个支付SDK,你不该按“网络层”“逻辑层”“UI层”来拆。应该按“扫码支付”“刷卡支付”“线上支付”来拆。每个业务模块内部,再去做自己需要的网络、逻辑、UI。这样改一个支付方式,不会影响其他。

拆分方式 问题点 推荐场景
按技术层拆分 业务耦合深,改一个功能动全局 不推荐,容易变成单体
按业务域拆分 内部有重复代码,但外部隔离好 多业务并存的SDK首选
按组件职责拆分 粒度细,依赖复杂 比较大的核心模块内部用

我个人习惯是:先画业务用例图。每个用例对应一个模块。用例之间尽量不要有循环依赖。有一次我拆一个电商SDK,硬是把“购物车”和“订单”拆成了两个独立模块。后来发现它们之间只有单向依赖,改起来爽多了。

高内聚低耦合:不只是口号

高内聚低耦合,你肯定听过。但怎么在代码里落地?

说白了,内聚就是模块内部的东西要拧成一股绳。耦合就是模块之间不要手拉手。

怎么衡量内聚度?我们可以看一个模块的“职责数量”。如果一个模块既负责网络请求,又负责数据缓存,还负责UI渲染,那它肯定内聚低。好的模块,只做一件事,并且做好。

怎么衡量耦合度?看依赖方向。A模块依赖B模块?B模块也依赖A模块?这就麻烦了。双向依赖一多,改A的前提是必须理解B。时间一长,谁也动不了。

我记得有一次做监控SDK,核心采集模块和上报模块之间出现了双向依赖。采集模块需要知道上报状态,上报模块又需要采集数据。这不是拆不开了吗?后来我加了一个“上报队列”做中间层,把双向依赖变成了两个单向依赖。问题就解决了。

核心原则:尽量让依赖形成“树形结构”,避免“环形依赖”。

依赖管理:把“硬耦合”变成“软协议”

说到依赖,很多团队的做法是“直接引用”。模块A直接new了一个模块B的对象。这看起来很直接,但实际维护起来特别痛苦。你想测试模块A?必须先有模块B。你换一个实现?得改所有调用点。

我建议用接口来隔离。

每个模块对外暴露的依赖,都通过接口定义。实现放在模块内部。调用方只依赖接口,不依赖实现。这样换实现的时候,只要新类实现了接口,其他代码完全不用改。

避坑指南:我曾经遇到一个团队,接口定义得倒是很完善,但每个接口后面都跟着十几行注释。后来发现,注释太多反而说明接口设计得不好。好的接口,读名字就知道用法,不需要看注释。

依赖管理还有一个容易忽略的点:依赖方向。

我推荐“依赖倒置”原则。高层的业务模块,不应该依赖底层的技术模块。反过来,底层的技术模块应该依赖高层定义的抽象接口。这样改底层实现,完全不影响业务层。

举个实际案例。我们做过一个日志SDK。一开始日志输出模块直接依赖了文件系统模块。后来想换成数据库存储?得改日志模块的代码。后来我们把存储抽象成一个接口。日志模块只调接口,不关心具体实现。换存储方式,完全不用改日志模块。

模块间通信:事件驱动 vs 直接调用

模块之间怎么交换数据?这也是个常见问题。

直接调用是最简单的。A模块直接调用B模块的方法。但这会导致耦合。如果B模块改了方法签名,A模块也要跟着改。

另一种是事件驱动。A模块只管发事件,不关心谁收。B模块或者C模块,自己决定要不要处理。这样A模块和B模块之间,连引用关系都不需要了。

我在项目里一般这样选:

  • 实时性要求高、调用关系稳定:用直接调用,但要通过接口
  • 业务流程多、调用链长:用事件驱动,写起来松散,维护起来方便
  • 跨进程或跨网络:用消息队列,但这种场景在SDK内部比较少

有一次我做跨模块的通知功能,用了事件驱动。结果发现事件在调试阶段很难追踪。后来我给事件加了一个全局ID,每次事件传播的时候都带上。这样日志里就能串起整个调用链了。这个技巧挺实用,推荐你试试。

本章核心知识图谱

下面这张SVG图梳理了模块化设计的核心逻辑。从业务出发做拆分,用内聚耦合来评估质量,再靠依赖管理和通信机制来落地。它就是我们这一章的知识骨架。

模块化设计核心知识图谱 模块化设计核心逻辑 模块化设计 模块拆分 高内聚低耦合 依赖管理 拆分原则 按业务域拆分 避免技术层拆分 每个模块只做一件事 评估方法 模块内部职责是否单一 模块间是否为单向依赖 避免双向耦合 落地技巧 用接口隔离依赖 依赖倒置原则 事件驱动降低耦合 目标:可扩展 · 可维护 · 可复用
注意:模块化设计不是一次性的。随着业务发展,模块边界会变。定期审视模块的依赖关系,发现有“环”就及时重构。不要等到依赖关系图变成蜘蛛网再动手,那就太晚了。

好了,模块化设计这个话题说起来不难,做起来是真的需要下功夫。我从一开始的“凭感觉拆”,到后来“按业务域 + 接口隔离”这套组合拳,走了不少弯路。你如果从现在开始就注意模块边界,会在后面很多章节里受益。