活动名额仍是 100 个,CR08 已让 R17 从“满员拒绝”变成“允许候补”。学生取消报名后,系统应核验队首资格、按序递补,避免同一空位重复发放。这条规则获批,并没有直接给出程序:申请信息经过哪些处理?谁安排递补?队列怎样保存?设计会议上出现的几张图,分别在回答这些问题。
先确定问题,再选择方法和图
分析把业务要求、系统边界和对象关系理解清楚,形成可交流、可检查的需求规格;设计在技术、成本和资源约束下决定实现方案。前者要澄清“取消后哪些人可以递补”,后者还要安排“哪个模块处理、接口传什么、怎样避免重复”。分析与设计可以反复衔接:发现资格规则有歧义,就应回到需求,而不是由程序员悄悄补定。
结构化与面向对象属于另一条轴。结构化方法先分解功能和数据变换;面向对象方法先组织对象的状态、职责和协作。采用哪种思路,都需要分析,也需要设计。于是,同一候补需求有四种观察位置:分析数据怎样流动、分析有哪些业务对象、设计模块怎样调用、设计类怎样协作。四格没有规定项目必须依次走四遍。

图名也不能独自决定阶段。分析中的类图可以只说明学生、活动和候补记录的业务联系,设计时再补接口、操作与存储映射。顺序图可以先表现参与者与系统的事件,再细化为内部对象的消息协作;IBM 的建模文档也分别说明其分析、设计和构建用途。教材的结构化分析还会画现状物理 DFD 和候选物理 DFD,帮助调查人、设备、介质和方案,不能见“物理”便排除分析。
从既有代码恢复理解:逆向工程(记录扩展)
如果接手的旧报名系统只剩代码,逆向工程就从现有软件恢复更高层理解。找出语句和数据细节属于实现级;找出模块、类或分量依赖属于结构级;说明一段程序如何完成资格过滤、更新队列,以及功能之间的关系,属于功能级;进一步把程序实体对应到“候补资格”“活动容量”等现实业务概念,属于领域级。层级看恢复了什么含义,同一张类图也可能表达不同抽象程度。
高层理解通常还需要规章、运行记录和历史决策,源码未必保存这些信息,所以恢复可能不完备;这不是严格的数学反比规律。从代码沿追踪链接找回 R17,是在使用既有对应关系;从代码推断“这个判断原来保护一项资格规则”,才是在恢复含义。两项工作可以配合。
数据、模块与数据库:同系统的不同模型
结构化分析(Structured Analysis,SA)把系统看作数据变换网络。教材从现状物理模型抽出逻辑模型,再按新需求建立目标逻辑模型,补数据字典和加工说明;随后提出候选物理方案,比较成本与风险、选择方案,整理需求规格。旧系统“由教务员查表”的动作,抽象后可成为“核验资格”;换成网页处理时,业务规则仍需要讲清。
数据流图(DFD)用外部实体、加工、数据流、数据存储四类元素说明数据的来源、变换和去向。以下只展开“申请候补”这一部分,并假设此例保留核验记录:资格加工读到学生资料,输出可入队的申请或拒绝结果并保存核验记录;入队加工读写候补记录,再向学生返回结果。取消和递补可在另一子图展开。
检查 DFD 要沿数据追问:加工有无合理输入和输出?每条数据流是否至少一端连接加工?外部实体直写存储、两个存储直接连线,都跳过了加工。教材完整模型中的存储应有读写来源;一个局部子图未画出的写入,可能在其它子图中,不能把局部只读视图直接判成整个系统错误。
展开父加工时,核对的是边界数据内容。父图的一条“候补申请=学生编号+活动编号”,在子图边界可以拆成两条,只要数据字典给出了这项组成,且内容没有丢失或凭空增加。箭头数量可以不同。加工编号表达层级和标识,不规定谁先运行;实际是否同步、并行,还要看实现与数据依赖。
数据字典(DD)给粗标签补语义:学生编号的类型和取值,候补申请的组成,候补记录的字段、存量和访问,数据流的来源和去向等。加工说明再写“满足哪些条件,应产生什么结果”。例如资格不符应拒绝入队;这一业务约束尚未指定哈希表或某种排序代码。DFD 看关系,DD 看定义,加工说明看转换规则,三者共同形成需求表达。
进入结构化设计(Structured Design,SD),问题转为模块。概要设计决定模块划分、接口与调用结构,模块结构图(SC)表达模块之间的控制关系;详细设计落实模块内部算法和局部数据结构。“调用资格模块”是模块结构,“读取资格后按条件选择”是内部控制,“资格信息送入加工”是数据流。看见箭头时,先读箭头的含义。
程序模块的三种基本属性分别问:功能“做什么”,逻辑“内部怎样实现”,状态“调用时需要哪些环境和条件”。资格模块的功能是给出资格结论,逻辑是按规则查询和判断,状态条件可以是资格资料已可访问。这里的状态属性与 UML 对象状态机各有语境。结构化程序设计(SP)用顺序、选择、循环组织控制,基本结构保持单入口、单出口。程序流程图表现控制步骤,NS 盒图和 PAD 强调结构化表达;PDL 用较固定的控制语句包住业务操作描述。当多个条件共同决定动作,判定表或判定树更方便检查组合;若有 n 个独立布尔条件,完整展开为 2n 种组合,业务约束和无关条件可以让规则合并,不能只按条件数机械填表。
ER 先说明业务联系,随后才决定表和索引
候补信息还要长期保存。数据库设计沿需求分析→概念设计→逻辑设计→物理设计→实现→运行维护推进,这条工作线可与模块设计配合。概念设计用 ER(实体联系)模型说明对象和联系,逻辑设计再映射为数据库支持的数据模型,物理设计才处理索引、文件组织等。数据库的“逻辑设计”与全系统的“逻辑分析模型”使用了相似词,各自处于不同工作线。
此图约定每一学生—活动组合只表示一条当前候补关系。“入队时间”属于这项联系:学生参加另一活动可以有另一个时间,不能只挂在学生实体上;多次退出再入队的历史要另加事件标识或历史结构。当前 M:N 联系在关系模型中通常用关联表,保存两端标识和联系属性。
若每项活动最多分配一个专用房间,房间也最多分配一项活动,该条件是 1:1;若一个院系可举办多项活动,每项活动只属一个院系,就是 1:N;学生与活动的候补则是本例 M:N。这里比较双方最大基数,是否必须参加是另一个最小参与度问题:1:1也可以允许暂未分配房间,不能读成双方必有一个。简单静态 1:N 联系常能在 N 端加外键;有历史、多次联系或不同约束时,独立联系表仍可能需要。最少表是特定转换题的条件,不能普遍当作数据库质量标准。
模块之间怎样连接,模块内部为何放在一起
模块边界定下来,还要检查两个指标:耦合看不同模块如何依赖,内聚看一个模块的活动为什么组成一体。把资格判断拆出去,并不自动得到低耦合:若递补模块直接改它的私有数据,边界依然脆弱。反过来,一个单一职责模块也可能依赖许多共享环境。高内聚、低耦合需要分别检查。

接口传学生编号,是简单数据;传完整学生记录,是复合结构,对应教材的标记耦合;传一个命令让对方选择“取消”或“入队”,就涉及控制耦合。布尔参数要看用途:把“已具备资格”当业务事实传入,与用开关指挥对方内部流程,判据不同。共享全局公共数据区是公共耦合,越过接口访问对方内部代码或数据则是内容耦合。
本教材七级中的通信耦合指多个模块共享输入或组合输出;非直接耦合允许模块经上层联系。其它资料常把共同受外部格式、设备或协议约束称为“外部耦合”,那是另一套常见谱系,不要悄悄换掉本教材表中的“通信”。同名的通信内聚则发生在一个模块内部。
判断内聚时,先看是否共同完成一项单一完整功能;功能内部自然存在执行顺序,仍可以是功能内聚。若题目只给出若干处理的输出接力关系,上一活动结果供下一活动输入,就是顺序内聚;若只要求先核验列表、后打印另一份报告,没有输出输入链,线索指向过程内聚;若两项处理都围绕同一组数据,线索指向通信内聚。不要用一个较弱特征推翻已经满足的更完整职责。
初始化配置、打开日志因为同一启动时机放一起,体现时间内聚,它们可以顺序执行。“各类输出功能放一起,由调用者选择哪一种”,体现逻辑内聚;随意拼凑无关操作,才是偶然内聚。换掉的是活动结合的理由,结论才随之改变。完整强弱序列留在速查,正文用这些条件帮助定位。
对象的责任:从分析模型到设计协作
面向对象把状态和相关操作组织起来。候补记录知道自己是否仍在排队,并提供合法的状态变更;外部通过接口操作它,这体现封装。类描述共同特征,对象是运行中的基本单元;对象之间发消息协作。继承表达一般与特殊关系,多态让同一接口根据具体对象执行相应行为,例如通知接口可分别交给邮件或短信实现。
教材采用 Coad/Yourdon 的 OOA(面向对象分析)五层:主题、对象类、结构、属性、服务。它们从不同关注面组织同一模型:主题把报名与通知分组,对象类识别学生和活动,结构说明分类及整体部分,属性描述状态,服务描述操作。这里没有五层运行时调用栈。教材五项活动从识别对象类开始,再识别结构、定义主题、属性和服务;它们可迭代,层次的列举顺序不等于工作步骤。
结构中的 is-a 分类与整体部分关系也要分开:“讲座活动是一种活动”表达分类,“活动具有候补记录”表达组成或关联,要进一步给出所有权等条件。设计阶段 OOD 延续分析模型,加入软件职责、接口、信息隐藏和可扩展机制;不能把每个现实名词直接翻译成一张表或一个实现类。
教材 OOA 九项原则还解释怎样控制模型复杂度:抽象保留本质特征,封装把状态与服务组织并隐藏细节,继承承接一般特征,分类把相同属性和服务的对象归为类;聚合组织整体部分,关联表达事物联系,消息通信把对象协作放在接口上。粒度控制让我们先看活动与候补整体,再进入记录细节;行为分析检查对象怎样响应事件、对象行为怎样相互影响。它们是建模原则,与五层组织、五项活动各自回答不同问题。
“界面控制”与“用例控制”中的控制,指向不同责任
窗口、外部协议或界面控制的职责分类,要看工作落在哪条边界。报名页面收取取消请求、检查输入格式、转换协议、显示结果,承担边界职责;候补协调器安排取消、资格核验、递补和通知,承担控制职责;候补记录维护入队时间、状态和合法变更,承担实体职责。

页面要“控制按钮是否可点”,仍在对接外部交互;协调器要“控制是否开始下一次递补”,涉及用例流程。把页面名字改成 UIController,责任不会随名字改变。某项资格规则可以放在实体或领域服务中,由控制对象安排调用;“做判断”本身不专属于控制类。
实体可以有方法,控制对象可以持有重试次数等属性;教材“通常持久”“通常方法较多”等倾向不能当语言禁令。抽象或具体则在问类能否直接实例化、怎样承接共同契约;边界类、控制类和实体类都可以选择相应实现形式。系统外的学生参与者,与系统内的学生业务对象,也不必一一对应。
OOP(面向对象程序设计)落实到对象、消息、封装、继承和多态。单继承/多继承按父类型来源数量区分;教材又按内容列取代、包含、受限和特化:取代强调继承能力后承接父对象的位置,包含强调完整承接父对象能力并获得其它能力。该段对受限和特化仅列名称,未给足操作定义,不能凭名字补成各语言通用规则。这套历史分类也不能据名称保证行为符合 LSP。多继承可能产生歧义,结果要看语言和继承结构。“包含继承”与 UML 用例的 include 各处于不同分类问题。
服务重启后,候补记录还要在
仅放在进程内存里的队列不会跨进程运行自动保留,持久化机制把需保留的状态保存到持久存储。采用关系数据库时,ORM(对象关系映射)协调对象属性、联系与关系数据访问,帮助分离业务操作和存储细节。一类可以映射多表,多类也可共用一表,具体映射取决于继承和数据结构。
持久层是承担保存与读取责任的逻辑层,业务代码通过它访问数据,减少对存储细节的依赖;ORM是在关系存储条件下协调对象与关系模型的一种映射机制。持久层可以采用 ORM,也可以采用其它数据访问方式,二者不等于两层必然相连的运行节点。
教材举 Hibernate 自动生成 SQL、iBatis 手写 SQL 映射、JDO 标准 API 等例,分别说明持久化支持方式,不代表当前使用量排名。持久化也不限关系库;概念 ER、设计类图和关系表可以对应,却不是三种完全相同的图。
UML:同一系统可以有多种提问(记录扩展)
UML(统一建模语言)提供共同表达语法,选择图时仍需选问题。用例看参与者目标和系统能力,类图看类型及联系,对象图看某时刻具体实例;活动图看动作和控制/对象流,状态机图看对象响应事件,顺序图看一次交互的消息先后,通信图突出协作连接和消息编号。4+1 视图从架构关注点组织模型,构件图则着重接口依赖,部署图表现运行节点与制品部署。
结构图/行为图按关注内容分类,与分析/设计的轴不同。活动图形似流程图,仍属于 UML 行为图;用例图属于行为类,但椭圆间连线不表达整套执行时序。活动语义使用节点、边与令牌流,不能简单定义成“高级状态机”。OO 方法也不绝对排除 DFD:OMT 功能模型就是反例。方法、图、观察目的,需要结合题干。
顺序图和通信图可以对照简单交互,但不能宣称任意复杂图无损互换。UML 2.5.1 §17.9把通信图对应到不含 InteractionUse/CombinedFragment 的简单顺序图,并给出消息超车无关等假设。通信图也可用编号等表示条件或迭代;区别不能缩成“一个有顺序,一个没有”。
用例的箭头先找端点,再读关系
申请候补与取消候补若都在适用步骤中执行账户检查,可把这项公共必需行为抽出,用 include 从基础用例指向被包含用例。如果账户已登录只是前置条件,就不必把“重新登录”强加为每次操作步骤。extend 从扩展用例指向基础用例,在扩展点插入附加行为,基础本身可独立完整;可声明触发条件,也可以省略显式条件。泛化从特殊者指向一般者,表示继承和特化,例如不同候补申请方式是一般申请方式的特殊实现。
整体部分的聚合/组合关系不是这些用例关系的替代名称。泛化可以连接类、参与者或用例,要按端点理解;“某章只列三种关系”也不等于语言中只有三种关系。另一套“结构事物、行为事物、分组事物、注释事物”在分模型元素;它与结构/行为两类图、14 种图类型属于不同分类问题。
还有一种“层”在描述语言自身:M0 是实际实例,M1 是所画业务模型,M2 定义 UML 的 Class、Association 等元模型,M3 用 MOF 等元元模型描述建模语言。这与业务模型内部的 OOA 五层分开。历史 UML 2.0 的 Infrastructure/Superstructure 都在描述语言元模型,不能望文生义映成 M2/M1。速查的 14 图清单明确采用 2.5.1;Profile 在清单中,Artifact 是制品元素,不能拿它补成独立第 14 类。
变化落在哪里,模式才有选择依据(记录扩展)
模式保存的是特定条件下处理重复问题的组织办法。架构模式处理系统组织,设计模式处理类与对象协作,惯用法处理特定语言实现;GoF 又按创建、结构、行为划分目的,按类/对象划分作用范围。前一组的“设计模式”并不等于后一组的“行为型”,Adapter 还同时存在类和对象实现。原书出版者的 Adapter 节选给出了继承与组合两版。

假设另一个活动需要换候补排序算法,可以把算法族封装成 Strategy;同一取消操作因记录处于排队、预留或确认状态而产生不同响应,可以用 State 组织行为。判据是算法替换和状态驱动行为,谁来选择不是唯一依据;普通条件语句也能实现这些规则,采用模式还要衡量变化频率与复杂度。
Bridge 面对两个需要分别演化的维度,例如活动类型与通知实现。抽象侧持有实现接口,加入一种活动或一种通知通道,可以分别扩展。Adapter 面对已有接口不匹配,例如把旧通知接口转换为系统期待的接口。Bridge 不要求图上一定有空心菱形;两种模式可以同时使用,不能只按使用早晚排除。
Decorator 在同一接口外增加附加职责,例如加一层通知;Proxy 控制访问,例如权限检查或延迟取得真实对象。它们的包装形状很相似,要看包装解决的困难。Decorator 即使“增加行为”,仍属于 GoF 结构型,不能把日常用词当目的类别。
Command 把请求封为可排队、记录和执行的对象;Memento 保存可恢复的状态而保护封装。撤销可能同时用到两者,只看到“撤销”不能唯一选择。Iterator 提供遍历接口,Visitor 为相对稳定的元素类型增加新操作,两者可以合作;若元素类型频繁增加,访问者的改动成本也会增加。
Factory Method 让子类决定具体产品类型;Builder 分离复杂构建过程,使同一构建过程得到不同表示。Mediator 集中封装对象间复杂交互,Observer 组织变化通知。题干只有“一个后端连接多个前端”,不足以唯一指定中介者,仍须交代交互是怎样发生的。
原则检查契约和变化代价
把候补业务与通知模板放在一个类,两项独立变化都会迫使它修改,SRP(单一职责)就提醒我们按变化原因分责任。OCP(开闭)通过扩展点减少稳定代码的修改,ISP(接口隔离)让客户端只依赖需要的接口,DIP(依赖倒置)让高层规则和低层机制共同依赖抽象。它们指导设计选择,不是禁止维护原代码的口令。
LSP(里氏替换)要求子类型维持客户端依赖的行为契约。例如通用资格检查接受正常学生编号,特化检查额外要求 VIP 令牌,就加强了调用前提;若原接口保证给出明确结果,子类型只返回未定义状态,又削弱了返回保证。Liskov 与 Wing 原论文还讨论状态和历史约束。语言允许的协变返回另有规则,不能要求结果必须完全相同;声明共同抽象基类,也不自动修复行为冲突。
组合可以通过接口保留多态,适合分别演化的协作对象;继承适合能满足替换契约的分类关系。LoD(最少知识)减少对无关对象内部结构的依赖:通知模块若一路穿过学生、院系、负责人再取得办公室,修改中间结构就会牵连它。私有属性、不可变对象和减少调用链是可能的手段;流式配置接口的链式调用不因长度自动违规。
回到那一项候补需求:先用分析把数据、规则和责任说清,再用设计决定模块、类、存储及协作机制。每张图选定一个问题,却用同名需求、状态和接口相互核对。能解释这些对应关系,才方便在5.4 测试中选择检查对象,判断方案和实现是否满足要求。