三种架构风格实现KWIC:主程序-子程序、面向对象与管道-过滤器对比

发布时间:2026/9/9 19:05:43
三种架构风格实现KWIC:主程序-子程序、面向对象与管道-过滤器对比 简介这是一份用于学习软件架构中三种典型风格的教学资源以经典KWIC索引系统为案例完整演示了抽象数据类型风格、调用返回风格与管道过滤器风格在实际工程中的实现差异适合软件设计课程学习者、架构初学者以及准备架构相关面试的开发者对照研读。压缩包内共57个文件以Java源码和编译后的class文件为主包含24个java、25个class以及5个txt文本txt文件承担输入输出与说明功能整体仅38KB轻量易用便于下载与分发。三种风格分别采用快速排序、插入排序和堆排序完成字母排序且均内置噪音词汇过滤机制过滤词的提取与修改十分直观输入文本通过input.txt灵活配置可反复更换语料观察不同架构风格在扩展新功能、调整排序策略时的改造成本从而深入理解各风格的可修改性、复用性与性能特性。工程基于MyEclipse 6.5编写导入IDE即可运行也可借助start.bat快速启动任意风格附带的readme与输出文件能帮助快速定位关键代码。已有1399人学习使用适合作为架构设计课后实践、课程设计与自学对照的参考资料。1. 先搞清楚KWIC到底在解决什么问题当年第一次在软件工程课上接到“用三种架构风格实现KWIC”这个作业时心里多少有点不以为然。KWIC全称Key Word In Context翻译成中文大概是“上下文关键字索引”名字听着绕本质不就是把每行文本的单词轮转一遍再排序吗一个字符串处理的小功能为什么非要费劲用主程序-子程序、面向对象、管道-过滤器三种架构风格各写一遍等我把三个版本都亲手实现出来才意识到这种“同一需求反复用不同架构风格实现”的练法比看十遍架构定义都管用。如果你正在学系统设计、准备架构相关面试或者只是好奇同一个需求为什么会有这么多种写法这篇文章应该能给你一个更落地的视角。1.1 需求其实只有三句话KWIC系统要处理的问题很朴素输入多行文本每一行看作一串按空格分隔的单词。对每一行把每个单词依次轮转到行首生成该行的全部轮转结果。把所有这些轮转结果放到一起按字典序排序再逐行输出。举个例子输入一行Software Architecture Styles轮转结果有三条Software Architecture StylesArchitecture Styles SoftwareStyles Software Architecture如果有多行输入就把所有行产生的轮转结果合并统一排序。看起来就是一个字符串处理工具没什么高深的。但注意经典教材里的KWIC在轮转后通常还会给每一行附上“原行标记”用来追踪这个结果究竟来自哪条原始记录工程实现上一般用一个原行号字段来维护。1.2 四阶段模型是三种风格的共同骨架不管用哪种架构风格KWIC最终都能映射到四个处理阶段行读取拿到原始文本去掉空行得到待处理的行集合。循环移位对每一行生成轮转集合。字母排序把所有轮转结果合并后按字母序排序。格式化输出把排序结果逐行打印。三种架构风格的差别不在这四个阶段本身而在于“谁控制执行顺序”“数据放在哪里”“未来需求变化时改动会落在哪个局部”。这四个问题才是架构风格真正要回答的。1.3 为什么KWIC成了架构对比的经典样例KWIC在软件工程教材里出场率极高原因不是它功能复杂而是它提供了恰到好处的“变化点”要不要过滤停用词排序是否忽略大小写输出要不要带原行号输入如果是几十万行文本数据能不能全部装进内存这些变化点每一个都真实可感改起来难度也不一样。同一个需求按不同原则切分代码应对变化的能力差距会非常明显。它就是一块标准化的“架构试验田”。2. 风格一主程序-子程序——最朴素的“流程拆解”2.1 核心思路以动词为中心组织系统主程序-子程序风格是结构化编程思想的直接产物。它把系统看成一组“动作”的集合先把大任务分解成若干子程序每个子程序干一件明确的事然后用一个主程序按固定顺序调用它们。整个系统里控制流高度集中数据一般通过参数和返回值在各子程序之间显式传递。用Python写出来就是一个主函数加四个纯函数def read_lines(raw_text): return [line.strip() for line in raw_text.splitlines() if line.strip()] def circular_shift(lines): shifted [] for line in lines: words line.split() for i in range(len(words)): shifted.append( .join(words[i:] words[:i])) return shifted def alphabetize(shifts): # 忽略大小写排序避免大写字母全部跑到前面 return sorted(shifts, keystr.lower) def output_lines(shifts): for line in shifts: print(line) def kwic_main(raw_text): lines read_lines(raw_text) all_shifts circular_shift(lines) sorted_shifts alphabetize(all_shifts) output_lines(sorted_shifts)运行时的数据流向非常清楚read_lines产生原始行circular_shift吞进去一个list吐出一个更大的listalphabetize收下并排序最后output_lines消费。子程序彼此之间不知道对方的存在全靠主函数在中间牵线。2.2 这种风格最舒服的场景好处是直观。代码摆在那儿任何人看一眼kwic_main就能完整复述系统在干什么不需要了解复杂的对象关系或接口约定。开发时调试也方便在alphabetize前后各打印一次结果中间数据什么样一目了然。对于一次性脚本、内部小工具、流程基本不会变化的场景这个风格是最省力的选择没必要硬上其他架构。我当时跑了一个简单输入生成的结果也符合预期输入 Software Architecture Styles Key Word In Context 输出 Architecture Styles Software Context Key Word In In Context Key Word Key Word In Context Software Architecture Styles Styles Software Architecture Word In Context Key2.3 一旦需求开始变化痛点立刻出现这个风格的麻烦在于“流程”本身成了整个系统的中心而流程恰恰是最容易被改动的部分。比如要加一个停用词过滤功能不允许In、Of这类词参与轮转你面临两个选择要么直接改circular_shift函数在轮转后把包含停用词的移位结果删掉这会让“生成轮转”和“过滤结果”两种职责纠缠在同一个函数里要么新增一个stopword_filter函数但这需要改动kwic_main主流程在移位和排序之间插一层调用。问题在于类似的变化会不断出现排序规则要改、输出格式要支持JSON、原行号要保留……随着子程序越来越多主函数里的调用序列越来越长参数列表和返回值的约定越来越复杂。最后这个“中心化控制流”会膨胀成一段谁也看不懂的面条代码。这是子程序风格与生俱来的瓶颈它把数据拆开了却没把变化隔离开。3. 风格二面向对象——让数据和它的行为待在一起3.1 核心思路以名词为中心把数据和操作封装起来面向对象风格换了种切法先找系统里的“名词”再安排它们之间的关系。在KWIC里天然存在的名词有一行文本、轮转生成器、排序器、输出器、还有总控台。每个名词都是一个对象数据被封装在对象内部只有对象自己的方法能修改它。class Line: def __init__(self, text): self._text text.strip() def circular_shifts(self): words self._text.split() for i in range(len(words)): yield .join(words[i:] words[:i]) class CircularShiftGenerator: def generate(self, lines): for line in lines: yield from line.circular_shifts() class Alphabetizer: def sort(self, shifts): return sorted(shifts, keystr.lower) class OutputPrinter: def print_shifts(self, shifts): for shift in shifts: print(shift) class KWICController: def __init__(self): self._generator CircularShiftGenerator() self._alphabetizer Alphabetizer() self._printer OutputPrinter() def run(self, raw_text): lines [Line(line) for line in raw_text.splitlines() if line.strip()] all_shifts list(self._generator.generate(lines)) sorted_shifts self._alphabetizer.sort(all_shifts) self._printer.print_shifts(sorted_shifts)和子程序版本最大的区别是数据不再以参数形式满世界传递。Line知道如何对自己的文本做轮转Alphabetizer知道怎么排序OutputPrinter知道怎么打印KWICController只负责组织它们的协作关系。“如何生成轮转”的细节被藏在Line和CircularShiftGenerator内部外部代码根本不关心实现。3.2 加一个停用词过滤器时改动落点完全不同现在再来看那个停用词过滤需求。面向对象版本的改法很明确新增一个StopWordFilter类只负责“判断一段移位是否包含停用词”这件事然后在KWICController的run方法里插入一步。轮转生成逻辑不用动排序逻辑不用动输出逻辑也不用动。好处其实不在于“少写了几行代码”而在于每个类的职责边界清楚修改点可以被单独测试。测试StopWordFilter时不需要关心Line的实现反过来的测试也一样。这就是面向对象风格常说的“封闭开闭原则”在起作用新增行为靠增加新类完成而不是修改老类。需要提醒的是这个例子里我用了组合而不是继承。Line内部包含字符串KWICController包含各组件对象没有任何一个类继承自另一个类。很多初学者总觉得面向对象必须用继承其实组合在这里更自然也更灵活。3.3 面向对象风格的真实代价接口必须先想清楚面向对象版本也有自己的麻烦类和接口的划分需要一开始就设计得靠谱。如果上来就把所有东西塞进一个巨大的KWICManager类方法十几个字段若干那它本质上只是把过程式代码换个壳子谈不上面向对象。反过来如果为了几十行的需求硬是抽象出一堆接口、工厂、依赖注入容器那就是过度设计维护成本反而比子程序版本更高。我在写这个版本时的一个体会是对象的粒度要跟着“业务概念”走而不是跟着“类名听起来高级”走。KWIC里真正有业务含义的概念就是行、轮转结果、排序、输出那就围绕它们建类不需要为了“以后可能扩展”提前做一堆目前用不上的抽象。4. 风格三管道-过滤器——数据自己是会流动的4.1 核心思路把系统当成一条数据流水线管道-过滤器风格把系统看作一条流水线每个过滤器只做一种数据变换过滤器之间通过数据流连接。前面过滤器的输出就是后面过滤器的输入数据像水流一样从源头流向终点。KWIC的四个处理阶段天然适合这条流水线。轮转过滤器专门负责把一行文本变成多条移位结果排序过滤器把收到的所有移位排好序输出过滤器逐行打印。用Python实现时生成器yield是构建管道最趁手的工具def line_reader(lines): for line in lines: line line.strip() if line: yield line def circular_shift_filter(lines): for line in lines: words line.split() for i in range(len(words)): yield .join(words[i:] words[:i]) def alphabetizer_filter(shifts): yield from sorted(shifts, keystr.lower) def printer_filter(lines): for line in lines: print(line) def kwic_pipeline(raw_text): stream line_reader(raw_text.splitlines()) stream circular_shift_filter(stream) stream alphabetizer_filter(stream) stream printer_filter(stream) # 只有开始消费 stream前面的生成器才会真正执行 for _ in stream: pass4.2 惰性求值带来的真实收益这段代码最值得注意的地方是惰性求值。line_reader产出一行文本circular_shift_filter会立刻针对这一行产出多条移位然后继续向上游要下一行。整个过程里任何时刻内存里都只保存当前正在处理的数据而不是像前两个版本那样必须先构造一个包含所有移位结果的完整list才交给排序器。这一点在数据量小的时候看不出来一旦输入变成几十万行文本差距就是“内存直接撑爆”和“稳定跑完”的区别。管道-过滤器在ETL任务、日志处理、编译管线和Unix命令组合这些场景里这么流行核心优势就是这个流式处理能力。组合性也比前两种风格更好。想要加停用词过滤写一个stopword_filter生成器插在circular_shift_filter后面其他代码不用动一行。想让输出同时支持文本和JSON直接在管道末端再分一路喂给不同过滤器即可。4.3 但有个坑排序过滤器破坏了纯数据流管道风格很优雅但也不是没有软肋。我实际写的时候发现alphabetizer_filter是这条流水线上最大的“堵点”——排序操作必须拿到上游的全部数据才能开始工作它做不到像其他过滤器那样来一条处理一条。这就意味着即使前面的过滤器都是流式的一遇到排序过滤器上游所有数据还是会被收集到内存里。这个问题的本质是有些操作天然是全局性的而纯数据流擅长的是局部性变换。工程上的应对方案很多数据量不大就容忍这个瓶颈数据量大了就把排序改成外部排序、分批归并或者干脆引入数据库。但在KWIC这种小系统里最简单务实的做法就是承认它继续跑因为大部分场景下数据规模根本到不了需要优化的程度。另一个实际调试中的经验管道版本的程序如果忘了最后那个for _ in stream循环所有生成器都不会执行程序会“静默成功”但什么都不输出。我第一次跑的时候被这个问题坑了十分钟检查了半天业务逻辑最后发现是少了消费动作。这也是惰性求值在调试阶段的代价错误不在代码里报出来而是藏在“没被消费的生成器”里。5. 三个版本跑完后我的选型结论5.1 三种风格的改变点对比观察维度主程序-子程序面向对象管道-过滤器数据组织方式通过参数和返回值显式传递封装在对象内部流式数据通道控制流归属主程序集中控制控制器负责编排对象协作过滤器链隐式驱动主要扩展方式修改主流程调用序列新增类/替换实现调整控制器插拔过滤器重排管道擅长场景一次性脚本、流程固定的小工具领域模型复杂、需要长期演进数据流处理、ETL、编译管线主要短板流程变化时牵一发动全身接口设计成本高容易过度抽象全局性操作如排序破坏流式特性调试有额外的“未消费生成器”风险这个表格是我自己整理出来的判断框架。注意“擅长场景”那一列没有哪种风格是“最好”的只有“在某个约束条件下更合适”。5.2 我在真实项目里实际怎么选先说结论如果功能基本等于一个脚本未来大概率不会变直接子程序风格别为了架构而架构。真实项目里大量内部小工具就是靠纯函数拼出来的运行稳定、上线快没必要给它套一个六类五接口的框架。如果这个系统要长期维护、持续迭代需求还会长出各种规则和状态那就在核心领域模型上用面向对象。这里的重点不是“用了类就是面向对象”而是把业务规则放在它应该属的地方让未来每一次需求变更都能落在尽可能小的改动范围内。如果核心指标是数据处理链路输入和输出很清楚、中间有一连串的变换和过滤比如解析日志、生成报表、处理消息流那管道-过滤器是最顺手的选择。同时要在设计阶段就识别出哪些过滤器是“全局性操作”它们会成为整条管道的瓶颈。实际系统很少只用一种风格常见做法是管道-过滤器搭起整体数据骨架骨架节点内部的复杂规则再用面向对象来建模。5.3 架构风格不是用来背的是用来做取舍的做完这个项目我最大的感受是架构风格的本质不是规定代码长成什么样子而是在决定“需求变化时你需要改动哪里”。KWIC项目足够小小到一个人能轻松驾驭三种写法因此也能直观地看到风格差异如何影响改动成本。如果你也想练手我建议严格按这个顺序来先写主程序-子程序版本再改成面向对象版本最后做管道-过滤器版本。你会切身体会到数据的位置怎么变了、控制流怎么变了、加一个新需求时改动的范围怎么变了。试过一次之后再跟人讨论架构风格脑子里浮现的就不再是抽象的定义而是“数据放在哪、流程归谁管、改需求时动哪个文件”这三个实打实的问题。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询