小叶的课程资料助手已经能展示一个基本过程:选一份文件,提出问题,得到回答,再打开引用。接下来,他想让引用在手机上更好读,并增加一种资料格式。小叶把两个要求交给 AI,却发现一次看起来很小的修改,牵动了许多地方。

换引用样式时,生成回答的代码也要改,因为它直接拼好了整块页面。增加资料格式时,提问入口、回答整理和原文查看又分别写了一套读取办法。每处单独看都不复杂,合在一起却很难判断:资料究竟由谁读,引用位置由谁认,改了这里又会影响哪里?

这就是本章要解决的结构问题。上一章确定了一次使用应当怎样发生、什么结果可以接受;现在需要进一步决定,哪些部分共同实现这些约定,每个部分负责什么,它们怎样合作,以及修改一处会影响哪些其他部分。

本章继续使用小叶和资料助手作为虚构教学背景,下面的程序组织、资料标识与变更都是新设的设计情景,不代表某个真实产品已经运行或验收。你不必先独立写出完整应用,可以先学会读懂一份设计,并判断 AI 提出的拆分是否解决了眼前的困难。

一、功能列表告诉我们要做什么,结构说明怎样共同做成

小叶最初列出的功能包括上传、提问、回答、引用和历史记录。这个列表很有用,它提醒开发者哪些使用行为需要支持。但它还没有说明这些功能背后的工作怎样组织。

例如,“显示引用”至少涉及几件不同的事:知道回答依据哪份资料,保存段落位置,让界面显示出处,以及在点击后取得原文。如果把它们都写在一个按钮的处理代码里,按钮确实可以工作;可是别的页面也需要同一种引用时,往往只能再复制一次。

系统结构关注组成部分以及它们之间的关系。某个部分提供资料内容,另一个部分利用内容组织回答,界面再把结果交给同学。这里的关系不是把几个框排整齐,而是要说明谁需要谁提供什么,谁对某项规则负责。

当这些决定影响到系统的整体行为、重要质量和后续改变时,我们也会称它们为软件架构。比如资料由一个统一入口取得,还是每个页面各自读取;模型调用放在固定边界内,还是散布在各个功能里,都可能影响开发者理解、测试和修改系统的方式。CMU SEI 对软件架构的介绍同样把整体结构、行为和相关设计决定联系起来。

在一个学生小项目里,不必为了使用“架构”这个词先画一套庞大图纸。小叶当前真正需要回答的是:换一种资料格式时,要让哪些部分知道?改手机引用样式时,为什么会碰到生成答案的代码?一次旧请求迟到,哪一处负责阻止它占据新问题的位置?这些具体问题足以开始结构设计。

同一个功能可以经过多个组成部分,同一个组成部分也可以服务多个功能。资料访问既可以为提问取得片段,也可以为点击引用找回原文。若要求“一个功能按钮必须对应一个模块”,就可能把本来应共同维护的资料规则再次分散。

反过来,“都是为了帮助学习”这个共同大目标,也不足以把所有代码放在一起。页面字号、文件解析和模型连接都服务同一产品,但它们需要了解的细节不同,变化的原因也不同。结构设计要在共同合作与分开处理之间找到有依据的边界。

二、先划清职责,再决定放几个文件

模块可以理解为围绕一组相关责任组织起来的部分。它应当有相对明确的工作范围,并通过约定的方式与其他部分合作。这里的模块是设计上的单位,未必恰好等于一个源码文件,也未必需要单独启动。

在程序设计课程中,我们已经见过把定义放进文件、由其他文件导入使用的做法。现在要向前再走一步:为什么这些定义应该放在一起,为什么其他地方应该共同使用它们?仅把一段很长的程序切成几个文件,还没有回答这些问题。

小叶可以先尝试以下四个逻辑模块。它们是当前小项目的一种候选划分,后面还要用使用过程和具体变更来检查。

模块 当前主要职责 对外的主要交接内容
界面交互 处理选择与提交,记录当前正在查看的问题,展示结果并提供回查入口 本次操作、请求标识,以及提交时选定的资料版本
答疑流程 在本次选定资料范围内组织取资料、生成候选回答和相应核对,形成带来源的结果 一次提问的处理结果,或明确的未完成、失败情况
资料访问 读取资料,维护版本、片段及原文位置的对应,提供取片段与回查操作 指定资料版本中的内容及可回查的位置
模型连接 按约定调用所用模型,转换调用和返回格式,报告已识别的调用问题 候选回答及本次调用的状态

这张表里的“负责”不是给程序取一个好听的名字。若资料访问负责段落与原文的对应,其他部分就不应再偷偷按自己的办法重新给同一份资料编号。否则,表里虽然写着统一负责,实际代码仍各管一套。

以一次具体交接为例。答疑流程请求某份资料的相关片段,资料访问返回片段文字,并说明它属于“资料07、版本1、段落3”。这几个标识用于认出资料及出处,并不是计算变量。答疑流程可以把这段材料交给模型连接取得候选回答,界面则根据最终结果提供回查入口。

同学点击该段引用时,资料访问需要按同一组标识找回原文。它不能只凭“段落3”猜测资料,也不能在文件更新以后把旧回答的引用悄悄指向新版本第三段。版本与段落对应虽然会在后面专章展开,但在这里已经是完成回查所必需的交接内容。

四个模块不等于四个人,更不等于四个 AI。小叶可以与一个 AI 一起编写全部模块;同一模块也可能由几个人协作。人员安排与软件责任可以相互参考,但不能因为今天有几个协作者,就决定系统必须切成几块。

模块边界的一个直接作用,是让小叶能更准确地安排任务。他可以说:“这次只改引用的展示方式,先核对界面怎样接收已有出处,资料标识与段落对应仍由资料访问提供。”这比“把引用做漂亮一点,其他地方你自己处理”更容易检查修改影响。

不过,一项职责写在表中,还不代表它已经被实现。特别是“相应核对”,需要说明当前究竟能检查什么:标识是否一致、引用位置是否存在,可以有明确程序规则;自然语言是否忠实于原文,还需要实际可用的内容核对办法。结构给这些工作安排位置,不能凭一个模块名字就保证回答正确。

三、沿执行顺序能找到工作,却未必找到合适的边界

面对陌生程序,小叶通常会先问:一个问题提交以后,接下来发生什么?这是很好的起点。按顺序追踪,能找到读取资料、选择片段、调用模型、组织回答和展示等工作,也容易发现漏掉的步骤。

但如果直接把每一步都做成一个模块,还要继续检查:这些模块是否共同依赖了某个本应由一处负责的内部细节?

设想最初只支持普通文本,内部用空行划分段落。读取步骤把分好的段落存成一张内部列表。选择片段的代码直接按列表位置找内容;回答整理的代码又用列表位置生成引用;原文查看的代码则重新按空行数去定位。程序大致按顺序运行,但三个地方都需要知道“段落就是内部列表的第几个元素”这项决定。

后来,小叶希望把资料存成另一种形式,以便保留段落标识和更多来源信息。文本含义没有变化,只是内部组织变了。此时选择片段、回答整理和原文查看都可能需要跟着改,因为它们知道的不是“向资料访问要一段内容”,而是“到那张列表的某个位置拿东西”。

问题不在列表这种结构本身,也不在程序有先后步骤。问题在于同一个存储决定被许多部分直接使用,一旦决定改变,它们就必须共同理解并配合修改。

一种更合适的划分,是让资料访问负责内部组织,对外提供“取得指定片段”“根据出处取回原文”等操作。答疑流程仍会先取资料再生成回答,执行顺序不一定改变;但其他部分不再需要依照内部列表的布局做事。

这就是为什么流程图与模块划分有关,却不能直接当成同一张图。流程图帮助小叶看到一次任务怎样推进;模块划分还要考虑谁掌握哪项决定、谁会因它变化而受到影响。

Parnas 在讨论模块划分的经典论文中提出,可以关注困难或可能变化的设计决定,再安排相应责任,而不只是依处理步骤拆分。论文的课程站转录保留了这一论证。本章借用这个判断方向,具体资料助手情景是另行构造的,不需要先学习论文中的旧式程序例子。

实际设计时,两种考虑经常同时存在。批量图片处理可以有读取、变换、输出的清楚阶段,同时把文件格式解释集中在读取部分。能按阶段组织,并不妨碍隐藏内部决定;关键是别把阶段名称当成边界已经合理的证明。

四、信息隐藏让别人知道怎样用,而不必跟着内部细节变

上面的做法称为信息隐藏。它关注的是:一个模块内部怎样完成工作,哪些决定需要让调用者知道,哪些可以留在内部改变。隐藏的目的,是减少外部对不必要细节的依赖,并保留实现上的选择空间。

这里的“隐藏”并不是保密,也不是不给协作者看源码。小叶当然可以阅读资料访问的实现,但界面若只需要打开原文,就不应该必须懂它的存储布局才能正确调用。MIT 的软件构造课程也用实现细节能否改变、调用者需要知道什么来解释这项原则。

可以从一个很具体的区别理解它。第一种交接是:“所有段落存在一张列表里,你自己按文件编号和位置拼出索引,再取文字。”第二种交接是:“给我资料标识、版本和段落标识,我返回这一段原文;找不到时会明确报告。”

第二种交接并没有省掉完成工作必需的信息。资料、版本、段落都保留了。它省掉的是外部不需要掌握的内部查找办法。资料访问以后改用另一种存储,只要仍能满足这些对外约定,调用者就有可能继续按原来的方式工作。

“有可能继续”还要落实到行为。若原来按段落标识能取得原文,新实现虽然保留同名操作,却开始把段落标识当页码解释,调用名称没变,约定已经变了。这时不能宣称内部替换对外没有影响。信息隐藏保护的是已经明确的使用关系,不是只保留几个函数名称。

模型连接也有类似作用。假设小叶先使用一种模型调用方式,候选文字放在返回结果的一个字段里。后来更换调用方式,文字所在字段和错误表示变了。如果界面、答疑流程、导出功能都直接解析供应方的原始返回,它们都需要知道这次变化。

若由模型连接集中处理这些差别,向外返回项目已经约定的候选文字和状态,其他部分可以少了解一种外部格式。但它不能用格式转换掩盖实际能力差别:新模型未返回依据,不能被转换成“依据已齐全”;调用超时,不能伪装成空字符串答案。

此外,调用方式变得相容,不等于回答表现完全相同。模型可能对同一材料给出不同解释。小叶仍要用资料与验收场景核对候选回答,不能把“替换集中在一个模块”当成“无需再验证系统结果”。

信息隐藏也需要边界判断。如果一个模块只返回“失败”,却把文件不可读、资料未就绪和模型未返回都混在一起,调用者可能无法给同学提供合适的下一步。为了不暴露底层细节,可以转换错误表达;为了让上层正确处理,仍须保留影响行动的区别。

因此,小叶可以把问题问得更具体:调用者为了正确使用,必须知道哪些条件、结果与失败状态?它现在知道的哪些细节,只是因为编写时图省事,被直接交给了外部使用?前者应讲清,后者才有理由留在边界内部。

五、内聚帮助我们判断,哪些工作值得放在一起

已经知道怎样隔开内部细节,还会遇到另一个问题:一个模块里面应该装多少事情?只说“每个模块做一件事”,往往不够,因为“一件事”既可以是读取一段文字,也可以被说成管理整个产品。

内聚关注同一模块内的工作彼此有多紧密的联系。SEI 的软件设计教学资料也将它与模块内部任务的相关性联系起来。对于当前项目,小叶可以看这些工作是否共同维护一组规则、是否需要共同理解同一种资料,以及是否经常因同一个原因改变。

读取资料、维护片段标识、按标识取得原文,虽然不是同一个按钮动作,却都需要维护“这段文字属于哪个资料版本、对应哪里”这一关系。把它们安排在资料访问的边界内,可以让编号与回查按一套约定工作。

相反,修改聊天气泡颜色与修正资料分段规则,通常不需要一起理解。若它们只是因为都在同一个页面文件里就被当成不可分的工作,修改者每次都要穿过很多无关内容。小叶可以把展示处理留在界面,把资料规则交给资料访问;界面拿到带有出处的内容以后,再决定怎样显示。

另一个常见例子是“工具模块”。项目里出现重复代码时,AI 可能建议把它们都放进一个名叫工具的目录。渐渐地,里面既有日期显示、模型调用,也有资料编号、页面配色。名字看起来统一,职责却没有共同中心。想改一个小规则,仍很难判断里面还有什么会受影响。

这不意味着项目里永远不能有小工具函数。如果几个调用者确实需要同一种独立文字处理,集中复用可以合理。需要留意的是,不要因为若干代码都比较短、都暂时不知放哪,就把它们误认成同一项责任。

内聚也不是把所有“与资料有关”的工作无限装进资料访问。现在的小工具可能适合一个清楚入口;以后如果资料检索需要独立优化,导入和长期存储也各有复杂规则,就可以在这个边界内部进一步分工。外部仍可以通过已经说明的能力来使用,内部不必始终只有一个文件。

判断进一步拆分是否值得,要看它减少了什么理解负担。若一个人只改读取格式,却必须同时理解查询排序,分开可能有帮助;若拆开以后每次修改段落标识都要到处协调,而外部使用方式没有变得更清楚,就要重新考虑划分。

内聚通常与耦合一起讨论。前者看模块内部为何放在一起,后者看模块之间怎样相互依赖。下一章会更具体地讨论数据流和依赖;这里先抓住一个方向:让共同维护的规则有清楚归属,同时减少其他部分对它内部做法的依赖。不是追求模块彼此完全无关,因为它们本来就要共同完成任务。

六、每块都能工作,还需要有人负责一次完整提问

划分了界面、资料访问和模型连接以后,小叶可能觉得只要分别做好,再接起来就行。但“接起来”本身包含一些不能留空的决定。

同学在资料07的版本1下提交问题,系统需要在这次请求中保持同一份资料范围。资料访问返回哪些片段,模型依据哪些片段生成文字,回答保留哪些出处,以及最终给哪个问题显示结果,都必须能接得上。

本章把组织这段工作安排给答疑流程。它接收本次问题及提交时选定的资料版本,要求资料访问按这个范围取得内容,再把问题和相应片段交给模型连接。候选回答回来后,它按当前已经明确的规则组织核对,将结果与本次问题、资料和出处联系起来。

这里没有要求流程模块亲自完成每项底层工作。它不需要自己解析每种文件,不需要直接理解供应方的所有返回字段,也不必拼手机页面。它负责的是一次任务怎样使用已有能力,以及什么时候可以把什么结果交给界面。

例如,资料尚未就绪,流程就应先说明资料还不能使用,本次无法继续基于它作答。再设想本次只允许依据提供的片段作答,候选回答却标出了一个不在这些片段中的出处。流程保留了本次片段清单,就可以对照标识发现这个问题,向界面说明暂时无法提供可回查的回答。即使出处在清单内,还要另行核对原文是否支持回答的意思;检查标识解决的是来源对应问题。这样就能看清:流程在组织哪项检查,检查结果又怎样影响后续交接。

再看上一章提过的旧结果迟到问题。界面最清楚同学当前正在查看哪一个问题,因此可以在发起时给本次请求一个标识,并记住当前请求。答疑流程在后续处理与返回中保留这个标识,以及提交时的资料版本。结果回来时,界面再检查它是否仍属于当前允许展示的请求。

如果同学已经切换资料并重新提问,旧结果就不应因为刚刚返回而盖住当前区域。单独比较资料名还不一定够,因为同学可能切走后又切回来,并提出另一个问题。需要对应的是这一次请求及其资料范围,不能只认相同文件标题。

这个安排同时需要合作和归属:界面负责当前显示状态,流程负责保留请求对应,资料访问负责资料版本与位置。只在表里写“防止混乱”,但没有任何部分实际作最后的对应检查,责任仍然悬空。

协调责任也不能变成一个包揽一切的总管。如果引用颜色、文件解析、供应方字段都堆进答疑流程,它又会成为每次变化都要修改的地方。小叶可以沿一次提问检查它调用什么,同时问:这里是在决定任务怎样推进,还是已经替另一个模块处理了应由它负责的细节?

这样,拆分才会形成可以协作的整体。各模块按自己的职责处理不同内容,同时保持这些内容与本次请求、选定资料的对应;还需要有人组织任务的开始、完成和失败处理,才能把局部工作接成符合要求的结果。

七、同一个系统,可以用不同的图回答不同问题

现在可以给四个模块画一张关系图。这幅图只表示本章讨论的内部职责与使用关系,外部模型服务、操作系统等环境没有全部画进来。

资料助手四个逻辑模块的职责与使用关系
图 S03-01|资料助手四个逻辑模块的职责与使用关系

图里的箭头从使用者指向提供能力的一方。界面使用答疑流程处理提问,也使用资料访问打开原文;答疑流程使用资料访问取得片段,再使用模型连接取得候选回答。箭头表示这些部分存在相应的使用关系,并不表示它们同时启动,也不表示每次操作都经过所有箭头。

比如同学只是打开一个已经存在的引用,这次操作可能只需要界面与资料访问配合,不必重新调用模型。若把静态关系图误读成每次都要走完的操作步骤,就容易给一个简单回查增加无关工作。

视图就是为回答某一类问题,选取系统中的相关部分及关系来描述。看责任归属时,关心模块怎样分、谁使用谁;看一次回答怎样完成时,则关心事件先后和传递内容。SEI 的 Views and Beyond 介绍也强调,系统可以有多种结构,描述应服务于使用它的人所要解决的问题。

下面再把一次正常提问及随后的回查展开。开始时,资料已就绪,同学选定资料07的版本1;这一次没有中途切换,也没有调用失败。图中向下表示先后,间距不代表实际耗时。

同四个模块在一次正常提问与引用回查中的交互顺序
图 S03-02|同四个模块在一次正常提问与引用回查中的交互顺序

界面先提交问题和对应标识,答疑流程取得片段,再交给模型连接。候选回答回来后,流程按已经确定的办法核对并组织结果,交给界面处理展示。同学随后点击某条引用,界面再请资料访问返回相应原文。这些动作把提问与回查连接起来,但不是说模型一返回,内容就自然得到了验证。

两张图中出现的是同四项责任,描述的关系却不同。时序图中资料访问把内容返回给答疑流程,不表示资料访问因此需要调用答疑流程的内部代码。返回结果是这次交互的一部分,不能见到一条反向消息,就直接认定静态依赖也反过来了。

这对阅读 AI 生成的方案很有帮助。若图中有一条线,可以要求它说明:这是使用关系、数据传递、执行先后,还是部署连接?如果几种含义混在一起,线越多,往往越难知道方案到底承诺了什么。

小叶目前不需要掌握所有画图规范。先让一张图只回答一类主要问题,写清箭头含义,再配一段具体经过,就足以发现不少责任空缺与错误交接。复杂图法可以以后按需要增加,不能让画图本身变成迟迟不开始实现的门槛。

八、逻辑上分开,不必立即放到不同机器上

四个模块的职责比较清楚以后,AI 可能进一步建议:“把它们分别做成服务,独立部署,方便扩展。”这个建议需要重新判断,不能从职责已经分开直接跳到部署也必须分开。

部署关注程序和相关资源实际放在哪里、怎样启动和运行。一个逻辑模块可以包含几个文件,几个逻辑模块也可以在同一个应用程序里运行。它们通过函数调用合作,仍然可以有明确的责任和接口。

本章的“模型连接”尤其要辨清。它是小叶应用里负责调用及转换结果的部分,不是模型本身。如果使用外部模型服务,模型可能运行在别处,但调用它的连接代码仍可以留在小叶的同一个应用里。外部能力的位置与项目代码的模块边界是两项不同决定。

对于目前的小项目,小叶可以先让四项责任保留在同一应用中。界面需要一次答疑时调用流程,流程需要资料或候选文本时调用相应模块。这能让开发者开始验证完整使用,而不用先安排几个服务怎样启动、怎样寻找彼此。

如果以后确实需要让不同课程共用同一个资料访问能力,或者需要独立安排某一部分的运行资源,再考虑把相应能力提供成独立服务。选择的理由来自具体使用和维护需要,不是因为“服务”比“模块”听起来更成熟。

分层也可以帮助理解责任的层次。例如,把界面如何展示、一次任务如何组织、资料和模型怎样访问,放在不同层次讨论。界面可以依靠下层提供的能力,不必知道每种文件怎样解析;任务流程可以使用连接能力,不必负责显示字号。

这里的分层不是强制每次调用都只能紧邻下一层。本例已明确界面打开引用时直接使用资料访问。若某个项目选择更严格的逐层约定,就要相应设计回查入口,并解释它解决什么问题;不能一边画出实际调用,一边又宣称所有调用都严格逐层。

分层的用途在于帮助判断哪些细节应该由哪里掌握,不能靠层数证明质量。让一个原本清楚的取片段操作经过许多只转交参数的中间层,可能让追踪更费力。是否保留某层,要看它是否承担稳定规则、必要转换或有意义的协调。

所以,小叶现在可以同时作出两个决定:逻辑上划清四项责任,部署上先保持简单。以后部署需要改变时,再检查原有交接是否适合新的运行方式。提前有清楚边界能提供便利,但不会自动解决网络等待、部分失败和更新配合等新问题。

九、拿具体变化试一试,边界到底有没有用

判断设计时,只看当前的一次顺利运行还不够。小叶最初遇到的困难就是修改时牵动太多地方,因此可以把已经预期的变化逐项代回当前划分。

第一项变化是手机上的引用样式。假设原文来源、段落标识和打开方式都不改变,只把横向来源条改成更容易阅读的卡片。此时主要工作应在界面交互:怎样显示标题、怎样安排点击区域、怎样让同学认出出处。资料访问仍返回相同的原文定位内容。

如果必须去模型连接里改 HTML 字符串,小叶就应追查:模型连接为什么在决定页面样式?它本来应交出候选文字及状态,却把展示方式也包进了返回。可以让它返回项目所需内容,再由界面负责排版,隔开两种不同的变化。

这项建议真正改变的是依赖:界面不再等待模型连接替它做好整块页面,模型连接也不再需要了解手机布局。并不是给原函数换个名字,或多建一个空目录,就获得了同样效果。

第二项变化是新增 PDF 资料。原先按空行分段的文本办法未必能够直接使用,资料访问需要处理新的读取与定位信息。但是否只改资料访问,还要看对外约定能否表达新情况。

若现有约定已经允许每个片段携带可回查的位置,PDF 导入可以把页码等位置保留在这项信息里,其他部分按约定使用。如果旧约定仅有一个含义固定的段落号,界面又需要新增页码展示,那么交接和界面也可能需要调整。不能仅凭“信息隐藏”四个字,承诺所有新格式都不影响外部。

小叶需要核对的是:新资料被读出了什么,片段是否保留正确出处,调用者是否仍能据此完成旧任务,以及新要求是否增加了旧约定没有表达的内容。这比数一数改了几个文件更接近结构好坏的实际含义。

第三项变化是内部存储替换。假设资料内容、版本与片段标识的意义都保持不变,只改变资料访问内部的存放方法。此时有理由期待影响主要留在该模块,但仍要检查外部是否曾绕过约定直接读内部列表,以及新实现是否确实保持原有行为。

第四项变化是支持两份资料比较。这时任务本身已经改变:选中范围不再只有一份,回答要区分两份依据,界面也要让同学知道正在比较什么。它可能涉及界面、流程、资料访问和核对规则的共同调整。这样的跨模块改变未必说明原划分失败,因为改变的本来就是多个部分共同承担的使用约定。

可以把这些区别简记如下。

变化 优先查看哪里 不能略去的条件
只改引用卡片的排版 界面交互 来源信息及打开方式仍满足原约定
内部存储方式替换 资料访问 内容、标识含义与对外行为保持一致,外部没有绕过边界
新增资料格式及定位信息 资料访问,再看交接与界面 现有约定是否足以表达新增位置与读取状态
更换模型调用方式 模型连接,再查回答行为 格式转换不能掩盖能力和结果差别
新增两份资料比较 沿新的完整使用过程检查 输入范围、回答归属和验收条件需要重新明确

这些检查并不要求小叶预测一切未来。他可以先选择已经打算做、或者当前最容易出问题的变化。一次具体变更若暴露出许多部分共同依赖同一内部细节,就获得了调整边界的理由;模糊地说“以后可能很复杂”,则还不足以支持增加大量结构。

十、模块太大和拆得太碎,都可能让工作更难

经过上面的分析,小叶可能想把每个容易变化的地方都独立拆出,似乎这样最稳妥。但边界本身也有成本:需要说明交接、转换结果、寻找实现,并在出错时追踪多个部分。

假如当前只有一种很简单的标题显示规则,每次调用都只是去掉首尾空格,小叶却给它单独建立请求层、调度层、转换层和返回层,那么修改者为了理解一次显示,反而要来回打开许多文件。增加结构没有隔开新的困难决定,只增加了阅读距离。

另一方面,若资料读取、问题处理、网络调用、页面排版全部挤在一个巨大函数里,每次改文件格式都可能影响页面行为,小叶也很难只检查其中一项。这是工作责任与内部变化纠缠的另一种困难。

因此,模块大小没有脱离任务的固定最佳数值。小叶可以看几个更具体的现象:为改一种规则,需要理解多少无关工作;几处是否总因同一决定同步修改;不同调用者是否偷偷依赖内部细节;一项失败能否沿交接找到发生位置。它们比“每个文件必须少于多少行”更能说明当前问题。

把模块变成独立的网络服务,还会增加另一类交接成本。同一个程序中的直接调用,通常可以沿调用过程跟踪;跨网络以后,请求可能尚未到达、结果可能迟迟未回,各部分也可能使用不同版本的约定。若确有独立运行需要,这些投入可能值得;若只是为了让一个学生小工具显得复杂,则应先问它解决了什么已有困难。

这并不意味着要永远等问题严重了才整理结构。小叶可以在当前需求已经显示出重复读取、格式泄露或职责混杂时,尽早建立清楚边界。只是投入应针对实际关系,而不是把可能用到的所有架构方式提前装进项目。

一种可行的推进方式,是先把一条能使用的提问过程接通,同时让资料访问和模型连接有清楚入口。遇到下一种格式或调用方式时,用前面的变更检查确认边界是否仍合适。这样既能获得真实使用反馈,也能逐步积累为什么这样划分的依据。

十一、让 AI 参与划分时,把判断依据交给它

AI 能很快根据功能列表画图、建文件、生成接口。小叶真正需要提供的,不只是模块名称,还有这次希望保护的关系和已经知道的变化。

例如,他可以说明:“目前每次只围绕选中的一个资料版本作答,引用要能回到当时原文。下一步先改手机引用样式,再增加一种资料格式。请说明哪些责任应该共同维护,哪些内部细节不应让界面知道,并用一次提问走通这些交接。”

这个任务仍有开放空间。AI 可以提出不同划分,但需要用相同的需求和变化解释理由。若它建议新增一个“智能中枢”,小叶就可以追问它具体提供什么、谁需要调用、它比原来的答疑流程多解决了什么,而不是因为名字显得完整就接受。

在已有代码中修改时,也可以让 AI 先找出现有依赖:哪些地方直接解析同一种文件,哪些地方直接读取供应方返回字段,哪些地方自己生成出处。找到实际位置以后,再讨论集中责任,才不容易出现“设计表已改,代码仍按旧方式工作”。

让 AI 分别完成几个模块时,交接约定应先足以支持合作。资料访问不能把“位置”理解成内部列表下标,界面却按页码展示;模型连接不能把空结果当调用成功,流程又把成功等同资料中确实有答案。各部分需要对同一个输入和结果使用相同含义,具体契约将在后续章节展开。

验收也可以有两个相连的观察范围。先看每个模块在约定输入下能否提供相应结果,再沿完整使用核对同学最终看到了什么。模块的局部检查帮助定位问题,完整场景检查帮助发现交接遗漏;不能只看 AI 分别报告“本模块完成”就认为系统已经能协同工作。

小叶还应保留少量关键理由。例如“资料读取与原文定位共同维护版本和片段标识”“模型连接不直接生成手机页面”“当前没有独立部署需要”。这些记录让后续协作者知道边界保护什么,也便于需求改变时重新判断,而不是把今天的方案变成不能讨论的规定。

十二、回到小叶这次要完成的两项修改

现在可以对开头的困难作一个具体安排。小叶先保留四个逻辑模块,并让它们在同一应用里合作;当前没有足够理由先拆成多个独立服务。资料读取与原文定位统一由资料访问负责,模型调用及返回转换集中在模型连接,界面处理当前请求与展示,答疑流程组织一次问题怎样完成。

手机引用样式的修改,先从界面如何接收已有来源信息入手。如果原来是模型连接直接拼页面,就先分清候选内容与展示责任,再调整卡片布局。检查应覆盖引用能否识别、能否打开正确原文,以及修改后正常回答是否仍能显示。

新增资料格式,则先在资料访问中处理读取和出处保存,再核对现有交接能否表达新的定位信息。足够表达时继续沿用;不够时,明确更新相应约定和使用它的部分。检查既要看新资料是否可用,也要看旧资料的提问与回查是否仍符合原约定。

这份安排还要通过实现和实际场景来检验。它的价值在于已经说明谁负责什么、什么变化应主要留在哪里、哪些交接仍需确认。以后若出现两份资料比较或新的运行要求,可以从这些已经明确的关系继续调整。

学会系统结构与模块划分,并不是记住一个永远适用的四框图。你需要能沿一次任务指出组成部分怎样合作,也能在条件变化时判断:这是某个模块的内部做法变了,还是共同使用约定变了;现在的边界是否减少了无关理解,又是否保留了完成任务必需的信息。

练习与参考分析

练习一:五个文件为什么仍像一整块程序

小叶把一个长程序切成五个文件,但每个文件都直接读取同一张内部段落列表,并各自按列表下标生成出处。后来列表结构改变,五个文件都要修改。能否凭文件已经拆开,就说模块边界已经有效?应当从哪里调整?

**参考分析:**文件分开只说明代码位置发生变化。五处仍共同依赖内部列表布局,资料组织这一决定没有被隔开。应先找清哪些操作真正需要段落内容和出处,再由资料访问维护内部组织及标识对应,向外提供能够完成这些操作的约定。

调整以后还要检查调用者是否确实使用这个入口,是否仍偷偷访问旧列表;新入口是否保留了资料、版本和原文位置。只加一个包装函数却继续把整张内部列表交给所有调用者随意解释,未必改变造成联动修改的关系。也不能因为边界改善,就省去结果核对。

练习二:只改一个模块,就一定没有影响吗

模型连接换了调用方式,向外仍返回候选文字和状态。代码确实只改了这一处,但新模型对资料给出的解释发生变化。开发者认为“接口没变,因此无需再检查回答”。怎样评价?

**参考分析:**集中转换可以使调用者不必跟着适配外部格式,这是边界的一项作用。但调用方式相容与回答行为相同不是一回事。需要核对新实现是否满足原有状态约定,再用相应材料检查事实、解释与引用支持关系。

如果新方式不提供某项原来依赖的能力,还应明确说明缺少什么,并判断能否补足或调整支持范围。不能用相同字段名掩盖能力缺失,也不能从只改一个文件推定全部验收条件仍满足。

练习三:新增页码,是否证明原划分失败

原系统的出处只包含资料标识、版本和段落标识。现在教师要求 PDF 引用还显示原始页码。资料访问能够取得页码,但原来对外返回的结果里没有这一项。小叶发现流程交接和界面也需要调整,于是认为“信息隐藏没有用”。这个判断合理吗?

**参考分析:**页码展示是新的外部要求,现有约定没有提供这项信息,使用它的部分需要相应变化并不意外。信息隐藏可以隔开内部解析或存储方法,不能保证对外行为新增时所有调用者都无需改变。

应区分两种情况:只在内部替换 PDF 解析方法,且对外内容和定位含义不变,影响可以尽量限制在资料访问;新增要展示的页码,则需要更新交接,并确认界面把它呈现给使用者。还应保持旧引用的含义,不把既有段落标识突然改解释为页码。

练习四:返回消息意味着反向依赖吗

一次交互中,答疑流程向资料访问索取片段,资料访问将结果返回。时序图因此有一条从资料访问指向答疑流程的返回箭头。能否据此认定资料访问在代码上必须使用答疑流程的内部功能?

**参考分析:**不能仅凭返回箭头作这个判断。时序图描述消息在一次交互中怎样往来,返回值可以只是原调用的结果,并不要求被调用者再去使用调用者的内部实现。要判断代码依赖,应核对各部分实际需要哪些定义和能力。

若实际实现采用另一种回调方式,可能存在不同的约定与依赖,需要据实现说明;题面没有提供它,不能自行补成事实。图的箭头含义应先说清,不能把返回方向自动搬到静态依赖图里。

练习五:相同资料,为什么仍要区分请求

同学在资料07下提出问题甲,尚未完成就切换到另一份资料,随后又回到资料07提出问题乙。旧甲返回时,界面只检查资料标识相同,便把答案显示在乙下面。四模块结构中,应怎样安排与检查相关责任?

**参考分析:**界面需要记录当前允许展示的是哪次请求;流程在处理和返回中应保留原请求标识及提交时的资料版本,不能在完成时借用当前页面的新状态。最后展示时,界面应据本轮约定核对结果是否仍属于当前请求。资料访问负责出处正确,但出处属于同一份文件还不能证明回答对应当前问题。

需要实际重现甲未完成、切走又切回、提交乙、甲迟到的顺序,并确认甲不占乙的位置,再检查乙能正常显示。若只画了四个框而没有任何地方执行对应检查,结构仍没有完成这项任务;具体怎样取消后台工作是另一项实现问题。

练习六:模块清楚以后,是否应立即独立部署

一个仅供小组使用的工具已经划清资料访问、模型连接和任务流程,当前在同一应用中正常合作。团队尚未提出分别扩展资源、跨项目共享或独立发布的需要。有人建议把每项责任都移到单独的网络服务,以证明设计足够模块化。现有信息能支持这个决定吗?

**参考分析:**逻辑边界并不要求网络分离。清楚的约定与责任可以在同一应用中保持;单独服务会带来部署、通信、故障和版本配合的安排,题面尚未给出能抵偿这些投入的具体需要。

可以先维持当前简单部署,保留逻辑边界,并记录将来什么变化会促使重新考虑。如果实际出现多项目共用、资源安排或发布节奏的需要,再比较新的收益和交接成本。这个判断是当前条件下暂不增加部署复杂性,不是断言所有独立服务都没有价值。