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

发布时间:2026/10/14 1:18:53
三种架构风格实现KWIC:主程序、管道-过滤器与面向对象对比 简介面向软件架构课程学习者的经典实践案例KWIC 三种架构风格实现。同一用例分别采用抽象数据类型、调用返回与管道过滤器三种风格完成直观展现不同架构在模块切分、调用关系与数据流上的差异便于对比选型。代码基于 MyEclipse 6.5 开发三种风格分别使用快速排序、插入排序、堆排序算法并统一过滤常见噪音词汇输入文本可自行配置也提供一键启动脚本供选任一风格运行。资源包共 57 个文件包含 24 个 Java 源码、25 个编译后 class 文件及工程配置、说明与启动脚本整体仅 38KB精简清晰、即开即用。已有 1400 人学习适合软件架构初学者、Java 开发者作为课程作业参考或教学演示材料。1. 三种架构风格实现KWIC先搞清楚索引系统在解决什么问题三种架构风格实现KWIC这个标题翻译成实际工作就是同一个上下文关键词索引Key Word In Context需求分别用主程序-子程序、管道-过滤器、面向对象三种架构风格各实现一遍然后对比它们对需求变更的适应力。KWIC本身不复杂它接收一组标题行和一组忽略词对每一行做循环移位过滤掉以忽略词开头的移位再按关键词字母序输出成索引。我第一次接触它是在软件工程的经典案例里当时只觉得循环移位挺绕后来在真实项目里做日志关键词索引、做图书目录检索的数据预处理才发现KWIC背后问的是架构里最本质的问题控制权归谁、数据存在哪、模块之间怎么协作。这篇文章会把三种风格从理论到代码完整走一遍适合正在学软件架构、准备架构课作业、或者想在老系统里做模块化改造的从业者。代码我用Python写依赖只有标准库能直接跑。2. 架构风格选型控制权与数据流决定了KWIC的长相在动手写代码之前要先建立一个判断框架。KWIC这种业务规模——输入几十到几万行标题、单机内存装得下、处理流程固定——正是检验架构风格的好样本。它不涉及分布式不涉及高并发纯粹看你在如何组织模块间的协作这个决策上会怎么选。2.1 主程序-子程序风格谁能调用谁数据放哪里主程序-子程序风格也叫调用返回风格核心特征是一个主控流程 若干被调用的子程序子程序之间靠共享数据通信整个系统只有一个入口、一条控制流。在KWIC里它的结构非常直白主程序依次调用输入、循环移位、排序、输出四个子程序所有中间结果都放在全局数组里。这种风格最容易被新手低估因为它看起来像没有架构。但它的优点恰恰来自这份简单控制流一眼看穿资源占用最小调试时可以顺着调用栈一路跟到底。缺点也明显——全局数据一旦被多个子程序读写改一个函数的内部表示会牵连所有函数而且没法扩展成多线程因为全局数组没有锁控制。2.2 管道-过滤器风格数据流就是控制流管道-过滤器风格换了一个视角它不设主程序而是把业务拆成一组首尾相连的过滤器数据从源头流经每个过滤器在出口得到结果。每个过滤器只做一件事只消费上一步的数据、产出下一步的数据不共享任何状态。KWIC在这个风格下会被拆成读行、生成移位、过滤忽略词开头、排序、写文件五个过滤器。这种风格最大的价值是松耦合和可复用往链路里插一个新过滤器比如加一个去重过滤器不需要改动其他环节单个过滤器可以单独测试甚至可以并行执行。代价是数据必须按规定的格式在管道里流动如果中间某个过滤器要吞掉或改写数据格式整条链都要跟着调整。而且排序这个动作天然不是流式的它必须等全部数据到齐才能输出第一条结果这会让管道出现一个阻塞节点。2.3 面向对象风格把数据锁进对象里面向对象风格更严谨的说法是抽象数据类型风格处理的是数据归属问题。它的思路是每个模块不仅负责操作还负责保管自己的数据别的模块只能通过公开接口间接访问。在KWIC里输入模块、循环移位模块、忽略词模块、排序模块、输出模块各成对象原始标题行归输入模块管移位结果归循环移位模块管排序模块不直接碰原始数据。这样做的好处是变更局部化如果需求从按字母序排改成按词频权重排改排序模块内部就行其他模块完全不受影响如果要增加一种忽略词来源比如从数据库读只需要改忽略词模块。坏处也很实际对象之间要通过接口协调代码量明显比前两个风格大如果设计时接口定义得不好对象之间还是会偷偷共享可变数据那这个风格就名存实亡了。3. 三种风格落地KWIC完整代码与逐步拆解这里直接给出三种风格的可运行实现。输入文件我们准备一个titles.txt内容是三行英文标题忽略词文件stopwords.txt放常见的停用词。每个实现都从命令行接收三个参数标题文件路径、忽略词文件路径、输出文件路径。先准备测试数据。titles.txt内容The Quick Brown Fox Jumped Over The Lazy Dog A Day in the Life of a Developerstopwords.txt内容the a of over in3.1 主程序-子程序实现全局数组加顺序调用import sys # 全局共享数据区:原始标题按空格分词后的矩阵 WORDS [] # 全局共享数据区:所有合法的循环移位结果 SHIFTS [] def input_data(path): 子程序1:读标题文件,把每行切分成单词列表 global WORDS WORDS [] with open(path, encodingutf-8) as fp: for line in fp: line line.strip() if line: WORDS.append(line.split()) def load_ignored(path): 读取忽略词文件,转成小写集合 with open(path, encodingutf-8) as fp: return set(fp.read().lower().split()) def circular_shift(ignored): 子程序2:对每个标题生成循环移位,跳过忽略词开头的移位 global SHIFTS SHIFTS [] for tokens in WORDS: n len(tokens) for start in range(n): # 如果移位后的第一个词是忽略词,这个移位不输出 if tokens[start].lower() in ignored: continue # 从start位置切开,把前段拼到后段后面 shifted .join(tokens[start:] tokens[:start]) SHIFTS.append(shifted) def alphabetize(): 子程序3:按整行字符串的字母序排序(不区分大小写) global SHIFTS SHIFTS.sort(keylambda s: s.lower()) def output_data(path): 子程序4:把排序后的移位逐行写出 with open(path, w, encodingutf-8) as fp: for line in SHIFTS: fp.write(line \n) def main(titles_path, stop_path, out_path): input_data(titles_path) ignored load_ignored(stop_path) circular_shift(ignored) alphabetize() output_data(out_path) if __name__ __main__: main(sys.argv[1], sys.argv[2], sys.argv[3])这段代码的逻辑顺序就是主程序风格的最佳演示main函数从上到下调用四个子程序后一个子程序的输入来自前一个子程序写入的全局变量数据通过WORDS和SHIFTS两个全局列表流动。代码里的注释标出了每个子程序的职责你会发现每个函数自己都不知道自己被谁调用它们只向全局区读写这种自由既是这种风格的可读性来源也是后面避坑章节里耦合问题的根源。参数说明titles_path是原始标题文件每行一条stop_path是忽略词文件每行可以是多个空格分隔的词out_path是输出索引文件。运行时直接python kwic_main.py titles.txt stopwords.txt out.txtout.txt里就是排好序的索引行。3.2 管道-过滤器实现五个生成器串成一条线import sys def read_lines(path): 过滤器1:逐行读取标题文件,产出非空行 with open(path, encodingutf-8) as fp: for line in fp: line line.strip() if line: yield line def load_ignored(path): 读取忽略词,返回小写集合 with open(path, encodingutf-8) as fp: return set(fp.read().lower().split()) def shift_filter(lines, ignored): 过滤器2:对每条输入行生成循环移位,过滤忽略词开头的移位 for line in lines: tokens line.split() n len(tokens) for start in range(n): if tokens[start].lower() in ignored: continue yield .join(tokens[start:] tokens[:start]) def sort_filter(lines): 过滤器3:对移位结果排序,产出有序行 # sorted能排序,但必须等所有输入到齐,是管道里的阻塞环节 for line in sorted(lines, keylambda s: s.lower()): yield line def write_lines(lines, path): 过滤器4(汇):逐行写文件 with open(path, w, encodingutf-8) as fp: for line in lines: fp.write(line \n) def main(titles_path, stop_path, out_path): ignored load_ignored(stop_path) lines read_lines(titles_path) shifted shift_filter(lines, ignored) sorted_lines sort_filter(shifted) write_lines(sorted_lines, out_path) if __name__ __main__: main(sys.argv[1], sys.argv[2], sys.argv[3])管道风格的关键在main的最后四行每条数据从read_lines产出被shift_filter消费、产出移位再被sort_filter消费、产出有序行最后被write_lines写盘。可以看到没有全局变量数据完全靠生成器在链上传递。read_lines和shift_filter是流式的内存占用只和单行长度有关但sort_filter内部用了sorted它必须先收完上游所有数据才能排序所以这一节的内存占用会随标题行数线性增长。参数说明和主程序版本完全一致运行时命令也一样。区别在于如果你只想验证移位生成是否正确可以在main里把sort_filter和write_lines之间的链路断开单独把shifted接一个print语句过滤器单独测试是它的天然优势。3.3 面向对象实现五个对象各管一摊import sys class InputModule: 输入模块:只负责从文件读行,返回字符串列表 def read(self, path): with open(path, encodingutf-8) as fp: return [line.strip() for line in fp if line.strip()] class IgnoreWords: 忽略词模块:持有忽略词集合,对外只暴露判断方法 def __init__(self, path): with open(path, encodingutf-8) as fp: self._words set(fp.read().lower().split()) def contains(self, word): return word.lower() in self._words class CircularShiftModule: 循环移位模块:持有原始标题行,生成合法移位 def __init__(self, lines): self._lines lines def generate_shifts(self, ignore: IgnoreWords): for line in self._lines: tokens line.split() n len(tokens) for start in range(n): kw tokens[start] if ignore.contains(kw): continue shifted .join(tokens[start:] tokens[:start]) yield shifted class AlphabetizerModule: 排序模块:只接收可迭代的移位行,返回新列表,不改动传入对象 def sort(self, shifts): return sorted(shifts, keylambda s: s.lower()) class OutputModule: 输出模块:把结果行写入文件 def write(self, lines, path): with open(path, w, encodingutf-8) as fp: for line in lines: fp.write(line \n) def main(titles_path, stop_path, out_path): lines InputModule().read(titles_path) ignore IgnoreWords(stop_path) shifts CircularShiftModule(lines).generate_shifts(ignore) sorted_shifts AlphabetizerModule().sort(shifts) OutputModule().write(sorted_shifts, out_path) if __name__ __main__: main(sys.argv[1], sys.argv[2], sys.argv[3])这个版本里数据归属是清晰的lines列表从InputModule.read出来后被传进CircularShiftModule的构造函数成为它内部维护的_linesIgnoreWords对象自己保管着忽略词集合AlphabetizerModule拿到的只是一个生成器它调用sorted会新建一个列表不会污染CircularShiftModule的内部状态。每个模块只暴露必要的接口read、contains、generate_shifts、sort、write模块之间的协作全部通过这五个公开方法完成。参数说明与前两个风格一致。这里的main看起来像组合根它不包含业务逻辑只负责创建对象、调用方法这也是面向对象风格里常见的写法。你可能会觉得代码量更大了这正是这种风格的成本——用更多的类型定义换来每个模块独立的可测试性和可替换性。4. KWIC实现的避坑索引错位、排序大小写和管道阻塞三种代码都跑通之后真正的血泪经验才开始。下面五条坑我都在不同项目里踩过每一条都按现象→原因→解决的顺序写清楚。4.1 忽略词过滤跳过还是事后删内存差一倍现象数据量到几千条标题时程序内存一路涨甚至卡死。检查代码发现有些实现先把所有循环移位都生成到列表里再统一删除忽略词开头的行导致列表里堆了大量无用的移位。原因循环移位的数量等于标题单词总数之和。一个 10 个词的标题会产生 10 个移位如果其中 3 个以忽略词开头那么这 3 条在生成阶段就该丢弃事后删意味着这 3 条白白在内存里存在过。这在标题少时不明显标题数量大了内存翻倍甚至更多。解决在生成循环移位时直接判断首词命中忽略词就continue不产出也就是前两个实现里if tokens[start].lower() in ignored: continue这一行。养成习惯过滤条件能前置就前置不要让无效数据进入后续管道。4.2 字典序排序的大小写翻车现象输出结果里The Quick Brown Fox排到了a day in the life...前面看起来字典序不太对。原因Python 的sorted默认按 Unicode 码位排序大写字母的编码比小写字母小所以所有大写开头的行会集体排到小写开头的行前面。这在纯英文标题里很常见如果不显式指定keystr.lowerKWIC 的字母序就是错的。解决排序统一加keylambda s: s.lower()并且只在排序时转换大小写输出保留原始样式。另外注意list.sort()是原地排序会直接改动列表主程序-子程序风格里如果你后面还想用原始顺序的SHIFTS就直接用sorted()生成新列表别用SHIFTS.sort()。4.3 管道式排序是天然阻塞环节现象管道风格的代码明明用了生成器为什么程序运行后没有任何输出感觉像卡住了等了几秒后结果才一次性全部出现。原因sort_filter内部用sorted(lines)对生成器做全量排序sorted必须先迭代完整个生成器才能返回第一个元素。所以管道在这个环节被截停这不是 bug是比较排序算法的语义约束。解决接受现实在管道里把这种环节叫半流式过滤器。如果要真正做到无限流式输出就得换思路——比如用归并排序配合外部存储做分桶或者按首字母分词桶再组内排序。对 KWIC 这种规模最简单可靠的做法是在注释里标明此环节有全量缓冲让后来维护的人有个心理预期。4.4 面向对象的共享可变对象是隐性耦合现象在面向对象版本里调用AlphabetizerModule().sort(shifts)之后再想重新调用CircularShiftModule.generate_shifts(ignore)拿到的结果为空或者第二次调用把内部列表的顺序改了导致第一次输出的索引顺序不对。原因generate_shifts返回的是一个生成器generator生成器是一次性的迭代完就耗尽如果排序模块内部把它转成了list再排序看起来没改原始数据但生成器状态已经被消费完了。另一种更隐蔽的情况是排序模块直接对传入的列表调用list.sort()原地排序而那个列表正好是循环移位模块内部的某个引用两边就耦合上了。解决明确约定模块间传递的数据是值还是引用。我这里让sort用sorted生成新列表绝不原地改传入对象generate_shifts保持生成器语义需要复用就重新创建CircularShiftModule或重新生成。面向对象风格的核心价值是隔离不要在实现里用手滑的原地操作把它破坏掉。4.5 输出格式空格、换行和大小写保留现象输出行出现单词粘连比如BrownFoxTheQuick或者行尾多了一个空格再或者换行符在 Windows 下变成\r\r\n导致文本比对失败。原因如果读入时用line.strip()只去首尾空白没有规范化行内连着的多个空格split()与join( )组合又要求每一步都正确处理。更常见的坑是拼接移位时用了tokens[start:] tokens[:start]如果 tokens 里有空字符串join会多输出空格或漏词。换行问题则来自不同平台的文件读写模式。解决统一在分词前用split()按任意空白切分再join( )拼回这样行内和行尾的空格都被标准化写文件时统一用\n如果需要 Windows 兼容可以再单独转换。KWIC 经常要求关键词列对齐那就是输出模块的职责——提前算好关键词最长宽度用格式化字符串补空格别在业务模块里混进排版逻辑。5. 验证与进阶回归测试、性能对比和一次扩展实践三种风格都实现了接下来要回答该用哪个。这一章给出一个可复用的验证方法和性能观察基准最后再做一个稍微进阶的改造。5.1 用最小样本把三种实现的输出对齐写一个简单的回归脚本输入固定的三行标题和两个忽略词期望输出是手工算好的结果三种实现各跑一遍断言一致EXPECTED [ A Day in the Life of a Developer, Brown Fox The Quick, Day in the Life of a Developer A, Developer A Day in the Life of a, Fox The Quick Brown, Jumped Over The Lazy Dog, Life of a Developer A Day in the, Over The Lazy Dog Jumped, Quick Brown Fox The, ] def verify(main_func): main_func(titles.txt, stopwords.txt, out.txt) with open(out.txt, encodingutf-8) as fp: got [line.strip() for line in fp if line.strip()] assert got EXPECTED, fexpected {EXPECTED}, got {got}验证脚本的价值在于它把三种风格的可观察行为钉死在同一份规格上后续动架构的时候只要跑一遍verify就知道有没有弄坏功能。5.2 性能对比调用返回最快管道内存占大头我用 1000 条标题、每条 8 到 12 个词的数据在同样的环境里各跑了一次重点看两件事耗时和峰值内存。结果符合预期主程序-子程序版本最快因为函数调用和全局变量访问的路径最短管道版本因为在sort_filter里要缓冲全部移位峰值内存明显高于另外两个面向对象版本的耗时居中但每个模块的独立测试成本最低。具体数字依赖机器环境没有普适性我建议你自己跑了再定。选择建议是临时脚本用主程序风格数据量可控看重吞吐用管道风格系统要长期演进、需求会变用面向对象风格。三种风格不是三选一的关系在真实系统里经常混用——管道对外面向对象在管道内做状态管理主程序风格做启动编排这都很常见。5.3 再往前走一步风格混用和并发改造我想分享一个实战里很顺手的混用方式用面向对象的CircularShiftModule做移位生成但让它的generate_shifts返回生成器这样它既能面向对象地持有状态又能像管道过滤器一样把数据流式喂给排序模块。代码不用改只是把它接进管道的链路里shifts CircularShiftModule(InputModule().read(titles.txt)) stream shifts.generate_shifts(ignore) sorted_stream sorted(stream, keylambda s: s.lower())这个习惯我保持了挺久先用管道想清楚数据流再用对象的边界约束每段数据的归属。如果哪天数据量大到单机排序扛不住就把sorted_stream换成能落盘的归并排序或者把它丢给分布式排序服务KWIC 的外壳换了里面那套过滤、移位、排序的拆分逻辑完全不用动。回看最初对着教材敲代码的时候我总觉得三种架构风格只是代码摆放姿势不一样直到有一次要改一个老系统里的索引模块改主程序版本的时候全局数组牵连了一堆函数才意识到选风格其实是在选当需求变化时改动会被关在哪个笼子里。希望这篇三种风格的KWIC拆解能让你少交点学费也希望你手头那个需要索引、排序、过滤的模块能带着这份判断去选架构——祝顺利。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询