加载中...

本篇要回答的问题:审视模块的业务边界时,应该用什么样的思维方式?以办公软件的 IO 子系统为实例,看它如何从"散落核心系统的存盘代码"经过 Visitor → IO DOM 两次迭代,并不断重新审视边界

上一讲"少谈框架多谈业务"强调架构就是业务的正交分解、接口是核心。本讲用一个真实案例(许式伟当年做 WPS Office IO 子系统)把这套思维落地——关键是发现接口中"过度的(多余的)约束",把它提高到通用普适场景去看。

IO 子系统的需求与初始架构

IO 子系统需求:读盘/存盘 + 剪贴板拷贝(存盘)/粘贴(读盘);要支持多种格式:

类别 格式 特点
流式文档(读盘+存盘) Word、RTF、HTML、纯文本 基于文本逻辑
分页文档(存盘) PDF、PS 基于分页显示

初始设计(反面):把 SaveWord/SaveRTF/LoadWord/LoadRTF 直接散落到 Span / Paragraph / TextPool / Document 每个类里。

关键判断:这类代码散落在核心系统各处、几乎每个类都要改——这叫全局性功能。它完全是核心系统的一部分,无法称为独立子系统。受 OOP “一切以对象为中心"思想 + MFC Serialization 机制的"毒害”,更加剧了这种倾向。

良好设计的两条要求:①核心系统功能要少(最小子集);②核心功能要能收敛。但读盘存盘需求是开放发散的(格式层出不穷,难以收敛),所以这个初始设计不好。

Visitor 模式(第一次迭代,失败)

Visitor 模式:为核心系统 Model 层提供一套遍历数据的接口,数据通过事件接收(StartDocument/StartParagraph/Characters/EndDocument…),存盘函数改为 SaveWord(stg, doc VisitableDoc)

好处 问题
核心系统提供统一数据访问接口,IO 子系统得以抽离 本质就是 SAX 模式(数据源从磁盘换成 Model 层而已),SAX 的缺点全有
Word/RTF 等模块彼此独立又能融洽配合(如 RTF→Word 转换很简单) 有预设访问逻辑,客户未必想以相同逻辑访问数据
—— 事件模型简陋,需求方诉求不匹配,被迫缓存数据等待,大量冗余代码
—— 接口抽象难理解,事件次序需长篇文档说明

关键判断:这是许式伟做 WPS IO 子系统第一版的真实方案,他自评非常失败。教训——KISS 提倡的简单不是接口外观的简洁,而是业务语义表达上的准确无歧义

IO DOM 模式(第二次迭代,成功)

第二次迭代改为基于 DOM:定义 IoSpan/IoParagraph/IoDocument 等接口族,存盘改为 SaveWord(stg, doc IoDocument)

关键设计 说明
两套 DOM IO DOM(IoDocument)+ 核心系统自己的 DOM(Document),二者几乎雷同
超集关系 理论上 Document 是 IoDocument 的超集;通过 Document.Io() 函数转为 IoDocument 体现

相比 Visitor,IO DOM 三大优势:

优势 说明
工程量更低 存盘读盘模块代码量下降
理解一致性更好 接口体现业务
更自然、避免惊异 核心 Model 本就通过 DOM 暴露,IO DOM 只是其子集,客户理解成本最低

洞见:在 DOM 基础上再提供 Visitor 是多余的——DOM 提供了极度灵活的数据访问接口,几乎能适应所有读取场景。Visitor 反而是核心系统为 IO 子系统提供的"专用插件机制",是额外成本。

回到最初的需求:边界还没审视完

过一遍用户故事就会发现需求并没全部解决——PDF/PS 等分页文档漏掉了。因为 IO DOM 是流式文档,没有分页信息。解法:引入排版(Render)环节产出 View DOM

Render(doc IoDocument) → ViewDocument
SavePDF(f, doc ViewDocument)
SavePS(f, doc ViewDocument)
文档层次 来源 用途
IO DOM 核心 DOM 的子集 流式文档存盘读盘
View DOM IO DOM 经 Render 排版而来 屏幕绘制(onPaint)、打印(onPrint)、PDF/PS 存盘

关键判断:如果做需求分析时没把这些需求关联性找出来,那就不是一次合格的需求分析。

不断重新审视边界

继续审视,还会发现更多"过度约束":

发现的边界问题 原接口(过度约束) 修正后
剪贴板:数据流不一定是文件 Save/Load(f *os.File, …) Save/Load(f io.Writer/io.Reader, …)
LoadFile 应返回识别出的格式 LoadFile(file, doc) error LoadFile(file, doc) (format string, err error)
数据源不止文件(还有 IStorage 等) SaveFile(file string, …) Save(src interface{}, format, doc) error(参考 Windows STGMEDIUM)
PDF/PS 非流式,不能用 IoDocument Save(dest, format, doc IoDocument) 留作思考题——需适当调整文档类型

洞见:剪贴板这个具体场景,本质是在提醒我们"发现模块接口中多余的约束"。把模块提高到通用普适场景看,即使没有剪贴板这个具体需求,也能及时发现 *os.File 是过度约束。但用了 interface{} 后,必须在文档层面补清楚支持/不支持什么,避免共识麻烦。

总结

本讲通过 IO 子系统的真实迭代,剖析了思考模块边界的方法。最重要的是职责:不同模块各做什么、如何耦合、耦合方式的需求适应性如何、实现者心智负担如何。

先把"是什么"回答清楚

概念 一句话说明
全局性功能 散落核心系统、难以剥离的功能(如读盘存盘)
Visitor 模式 本质是 SAX,事件驱动;心智负担大,被判失败
IO DOM 核心 DOM 的子集接口族,自然且避免惊异
View DOM IO DOM 经 Render 排版而来,支持分页/打印/PDF
过度约束 接口里多余的具体约束(如 *os.File),应提至通用场景

一句话速记

审视模块边界 = 把接口提高到通用普适场景,揪出每一个"过度约束"——IO 子系统从 SAX(Visitor) 到 IO DOM 的迭代,证明 KISS 的简单是业务语义准确而非外观简洁。

几条值得记住的判断

  • 全局性功能散落核心、难剥离,要警惕 OOP "一切以对象为中心"的毒害。
  • Visitor=SAX,事件模型简陋、心智负担大;DOM 更自然,DOM 之上再加 Visitor 多余。
  • 合格的需求分析要找出需求关联性(如 PDF→View DOM→排版)。
  • 不断揪出接口的过度约束*os.Fileio.Writer、补返回 format)。

思考题

Save(dest interface{}, format string, doc IoDocument) 这个入口接口,在要同时支持流式文档(IoDocument)和分页文档(需 ViewDocument)时,最后一个参数该怎么调整才能既覆盖两类文档、又不引入过度约束?把你的接口签名写出来,并说明用什么机制做插件式格式扩展。

公告栏
这是我的个人知识库。
记录技术,也记录生活 —— 读过的、试过的、想明白的,都堆在这儿。
最新文章
网站资讯
文章数目 :
5
已运行时间 :
本站总字数 :
15.7k
本站访客数 :
本站总访问量 :
最后更新时间 :
全局知识图谱
当前页面 已访问 文章 标签
ESC 关闭 · 滚轮缩放 · 拖拽移动 · Ctrl+G 开关