2021 年软件系统设计考题解答¶
来源:
exams/2021 痛苦回忆.pdf「软件系统设计 2021 年春期末回忆」 注:本考卷为学生回忆版,题目表述可能与真实考卷有差异。"两小时时间很紧、一张答题纸不够写!!"
1. 软件模式是什么?能提供架构吗?¶
软件模式的定义¶
软件模式(Software Pattern) 是将模式的一般概念应用于软件开发领域,即软件开发的总体指导思路或参照样板。软件模式并非仅限于设计模式,还包括架构模式、分析模式和过程模式等。在软件生存期的每一个阶段都存在着一些被认同的模式。
软件模式的基础结构由 4 个部分构成:问题描述、前提条件(环境或约束条件)、解法和效果。
软件模式能提供架构吗?¶
可以,但取决于模式的层次:
- 架构模式(Architecture Pattern) 直接提供架构层面的指导,如 MVC、分层、Broker、SOA、微服务等,这些模式描述了系统的顶层结构和组件关系
- 设计模式(Design Pattern) 提供的是详细设计层面的解决方案,不直接构成架构,但多个设计模式的组合可以影响架构
- 软件模式与具体的应用领域无关,在模式发现过程中需要遵循大三律(Rule of Three)——只有经过三个以上不同类型的系统的校验,一个解决方案才能从候选模式升格为模式
参考课件:
slides/00概述.pdf(软件模式定义与分类)
2. 享元模式和原型模式为什么在不同的分类?¶
设计模式的分类体系¶
设计模式有两个分类维度:
| 分类维度 | 类别 | 说明 |
|---|---|---|
| 按目的(Purpose) | 创建型(Creational)、结构型(Structural)、行为型(Behavioral) | 模式用来做什么 |
| 按范围(Scope) | 类模式、对象模式 | 处理类之间关系还是对象之间关系 |
享元模式(Flyweight)与原型模式(Prototype)的分类¶
| 维度 | 享元模式(Flyweight) | 原型模式(Prototype) |
|---|---|---|
| 按目的 | 结构型模式(Structural) | 创建型模式(Creational) |
| 按范围 | 对象模式 | 对象模式 |
| 分类不同的原因 | 主要用于处理对象的组合结构——通过共享技术有效地支持大量细粒度对象,关注的是对象的结构组织 | 主要用于创建对象——通过复制原型实例来创建新对象,关注的是对象的创建方式 |
核心区别:享元模式关注"如何组织已有对象以节约资源"(结构型),原型模式关注"如何创建新对象"(创建型)。虽然都是对象模式,但目的不同导致它们被归入不同的类别。
参考课件:
slides/00概述.pdf(设计模式分类表),slides/04创建型模式.pdf(原型模式),slides/09结构型模式.pdf(享元模式)
3. 依赖倒转原则是什么?如何反映在设计模式中?¶
依赖倒转原则(DIP)定义¶
依赖倒转原则(Dependence Inversion Principle, DIP):
- 高层模块不应该依赖低层模块,它们都应该依赖抽象
- 抽象不应该依赖于细节,细节应该依赖于抽象
- 通俗表述:要针对接口编程,不要针对实现编程(Program to an interface, not an implementation)
在设计模式中的反映¶
| 设计模式 | DIP 体现 |
|---|---|
| 策略模式(Strategy) | Context 依赖于抽象的 Strategy 接口,而不依赖具体的策略实现类。Duck 持有 FlyBehavior 接口引用,而非 FlyWithWings 具体类 |
| 工厂方法模式(Factory Method) | Creator 依赖于抽象的 Product 接口,具体产品通过工厂子类创建 |
| 观察者模式(Observer) | Subject 依赖于抽象的 Observer 接口,不关心具体观察者的实现 |
| 模板方法模式(Template Method) | 抽象类定义算法骨架,具体步骤由子类实现,高层算法不依赖低层实现 |
核心思想:DIP 是实现开闭原则(OCP) 的主要手段。通过依赖抽象而非具体实现,可以在不修改现有代码的情况下扩展系统功能。
参考课件:
slides/01面向对象设计原则.pdf(依赖倒转原则),slides/02策略模式.pdf
4. 命令模式的适用环境是什么?¶
命令模式(Command Pattern)适用环境¶
使用命令模式的情况:
- 需要将请求封装为对象:在不同的时刻指定、排列和执行请求
- 需要支持撤销操作(Undo):Command 对象可以存储状态以支持撤销。Command 接口通常需要定义
execute()和undo()方法 - 需要支持日志和事务:将操作记录为日志,在系统崩溃时可以重新执行
- 需要支持宏命令(Macro Command):将多个命令组合成一个复合命令
- 需要将调用操作的对象与知道如何实现该操作的对象解耦:Invoker 不直接知道 Receiver,通过 Command 对象间接调用
- 需要在不同时间指定、排队和执行请求:Command 对象的生命周期可以独立于原始请求
命令模式结构¶
┌──────────┐ ┌─────────────┐ ┌──────────┐
│ Invoker │────▶│ <<interface>>│◀────│ Client │
│(调用者) │ │ Command │ │(客户端) │
└──────────┘ ├─────────────┤ └──────────┘
│+ execute() │
│+ undo() │
└──────┬──────┘
△
│
┌──────┴──────┐
│ConcreteCmd │
├─────────────┤
│- receiver │────▶┌──────────┐
│+ execute() │ │ Receiver │
│+ undo() │ │(接收者) │
└─────────────┘ └──────────┘
参考课件:
slides/05状态与命令模式.pdf(命令模式)
5. 隔栏和断言的目的是什么?¶
"隔栏"即隔离程序(Barrier),"断言"即Assertion,都属于防御式编程的范畴。
防御式编程的核心思想¶
可以预见到(至少预先推测到)问题所在,断定代码中每个阶段可能出现的错误,并作出相应的防范措施,来防止类似意外的发生。
断言(Assertion)的目的¶
- 在开发期间使用,让程序在运行时进行自检
- 是对开发人员的警告,通常是一个子程序或宏
- 断言不可以有副作用——不能影响程序的正常逻辑
- 帮助开发人员在开发阶段尽早发现程序中的逻辑错误
隔离程序(Barrier/隔栏)的目的¶
- 以防御式编程为目的而进行隔离
- 将某些接口选定为 "安全"区域的边界
- 对穿越安全区域边界的数据进行合法性检验
- 将校验工作集中在特定的模块中以降低成本
- 防止错误数据从一个模块传播到另一个模块
断言 vs 错误处理的区别¶
| 维度 | 断言(Assertion) | 错误处理(Error Handling) |
|---|---|---|
| 使用时机 | 开发期间 | 运行时(生产环境) |
| 目的 | 发现程序逻辑错误 | 处理预期的异常情况 |
| 对象 | 对开发人员的警告 | 对用户错误、程序错误的处理 |
| 副作用 | 不应有副作用 | 可以包含恢复逻辑 |
参考课件:
slides/00概述.pdf(防御式编程相关),exams/软件系统设计-复习-EagleBear.pdf
6. 什么种类的需求对架构很重要?哪里具体说明了这种需求?¶
对架构很重要的需求¶
ASR(Architecturally Significant Requirements,架构攸关需求) 是对架构很重要的需求。ASR 是对软件体系结构产生深远影响的需求。如果需求影响关键架构设计决策的制定,那么根据定义它就是 ASR。
主要包括: 1. 质量属性需求(Quality Attributes):如可用性、性能、安全性、可修改性、可测试性、可用性等 2. 核心功能需求:影响架构分解方式的关键功能 3. 约束条件(Constraints):技术限制、商业限制、法律法规等预先指定的决策
在哪里具体说明¶
这些需求通常在以下地方得到具体说明: 1. 需求文档:通过 MoSCoW 方法、用户故事等方式记录 2. 质量属性场景(Quality Attribute Scenarios):以"刺激-响应"格式精确描述质量属性 3. 效用树(Utility Tree):将质量属性分解为可量化的叶节点(场景) 4. 架构文档:在架构设计文档中明确记录 ASR 及其满足方式
参考课件:
slides/2026SUG_SysArch2_quality attributes.pdf,slides/Lecture 01 - Attributes Driven Design.pdf
7. 为什么设计决定对架构很重要?说出你对"软件架构=设计(如,组件-连接器)+设计决定"的理解。¶
为什么设计决定很重要¶
- 设计决定(Design Decisions)是架构的核心——架构不仅是系统的结构,更是一组有意识的选择
- 设计决定记录了为什么选择某种结构,而不仅仅是描述结构本身
- 设计决定包含了权衡(trade-offs)——为什么选择 A 而不是 B
- 设计决定使架构可以被评估、追溯和演化
对"软件架构 = 设计(组件-连接器)+ 设计决定"的理解¶
- 组件-连接器(Component-Connector)描述的是"What":系统的运行时结构——有哪些组件、它们如何交互
- 设计决定描述的是"Why":为什么这样组织组件、为什么选择这种连接方式、有哪些替代方案被排除
完整的架构不仅描述系统是什么样子(结构),更重要的记录为什么是这个样子(决策理由),这样后人才能理解设计的意图,也才能在需求变化时作出合适的修改。
参考课件:
slides/2026SUG_SysArch1_introduction.pdf(架构定义)
8. 架构一般设计策略是什么?以 ADD 为例说明这些策略如何体现。¶
架构一般设计策略¶
六种策略:分解、抽象、逐步求精/分而治之、生成与测试、迭代-增量细化、复用元素。
以 ADD 为例说明策略体现¶
| 策略 | 在 ADD 中的体现 |
|---|---|
| 分解(Decomposition) | ADD 步骤 2:选择要分解的系统要素,将系统逐步分解为子元素 |
| 抽象(Abstraction) | ADD 步骤 3-4:将系统元素抽象,关注其 ASR 和设计决策而非实现细节 |
| 逐步求精/分而治之(Stepwise Refinement) | ADD 步骤 5-7:为每个实例化元素分配职责、定义接口,逐一处理 |
| 生成与测试(Generate and Test) | ADD 步骤 4-⑥:评估并解决不一致的问题,验证设计是否满足 ASR |
| 迭代-增量细化(Iterative Refinement) | ADD 步骤 8:重复步骤 2-7 直到满足所有 ASR |
| 复用元素(Reuse of Elements) | ADD 步骤 4-③:从候选模式/决策清单中选择已有的模式或决策 |
参考课件:
slides/Lecture 01 - Attributes Driven Design.pdf(ADD 方法)
9. 组件-连接器样式的本质是什么?以 SOA 模式为例说明。¶
C&C 样式的本质¶
组件-连接器(Component-Connector, C&C)风格关注的是运行时行为:
- 组件(Component):主要的处理单元和数据存储,是计算的主体
- 连接器(Connector):组件之间交互的路径和机制
- C&C 描述的是动态的、行为的视图,而非静态的代码组织
以 SOA 为例说明¶
SOA 作为一个 C&C 风格实例:
┌──────────┐ HTTP/SOAP ┌──────────┐ HTTP/SOAP ┌──────────┐
│ Service │◀────────────────▶│ ESB / │◀────────────────▶│ Service │
│ Consumer │ │ Registry │ │ Provider │
│(服务消费者)│ │(注册中心)│ │(服务提供者)│
└──────────┘ └──────────┘ └──────────┘
- 组件:服务消费者(Service Consumer)、服务提供者(Service Provider)、服务注册中心(Service Registry/ESB)
- 连接器:HTTP/SOAP 协议通信、服务发现调用
- 运行时行为:消费者通过注册中心查找服务,然后与提供者建立通信
参考课件:
slides/2026SUG_SysArch1_introduction.pdf(C&C 风格),slides/软件架构模式_update_2.pdf(SOA)
10. ATAM 每个阶段的输出¶
| 阶段 | 输出 |
|---|---|
| 阶段 -0:准备和建立团队 | 评估计划(人员、时间、地点、评估范围) |
| 阶段 -1:评估-1 | ① 架构的简明介绍 ② 业务目标(驱动因素)的阐释 ③ 质量属性要求的优先级列表 ④ Utility Tree(效用树) ⑤ 风险和无风险点列表 ⑥ 敏感点和权衡点列表 |
| 阶段 -2:评估-2 | ① 涉众的优先级场景列表 ② 风险主题和每个受到威胁的业务驱动因素 |
| 阶段 -3:后续 | 最终评估报告 |
参考课件:
slides/Lecture 01 - Attributes Driven Design.pdf(ATAM),slides/Lecture 02 - Attributes Driven Design - Case Study.pdf
11. 设计一个大学通知系统。大学由若干个系组成,每个学生和老师都属于某一个系,形成树状结构。大学管理员想给一些部门或一些个人发送通知。问用什么设计模式?画出类图。¶
设计模式选择:组合模式(Composite Pattern)¶
原因:大学→系→学生/老师形成树状结构(部分-整体层次),管理员可能对整个部门(系)发送通知,也可能对个别学生或老师发送通知。组合模式允许客户端统一对待单个对象(叶节点)和组合对象(枝节点)。
类图¶
┌──────────────────────┐
│ <<interface>> │
│ UniversityComponent │
├──────────────────────┤
│ + sendNotification( │
│ message: String) │
│ + add(child: │
│ UniversityComponent)│
│ + remove(child: │
│ UniversityComponent)│
└──────────┬───────────┘
△
│
┌───────┴───────────┐
│ │
│ (implements) │ (implements)
│ │
┌──┴──────────────┐ ┌─┴───────────────┐
│ Department │ │ Person │
│ (系/部门) │ │ (个人 - 叶节点) │
├─────────────────┤ ├─────────────────┤
│ - name: String │ │ - name: String │
│ - children: │ │ - role: String │
│ List<Component>│ │ (Student/Teacher)│
├─────────────────┤ ├─────────────────┤
│+ sendNotification│ │+ sendNotification│
│ (msg): void │ │ (msg): void │
│+ add(comp) │ │+ add() - 不支持 │
│+ remove(comp) │ │+ remove() - 不支持│
└─────────────────┘ └─────────────────┘
代码示例¶
interface UniversityComponent {
void sendNotification(String message);
default void add(UniversityComponent c) { throw new UnsupportedOperationException(); }
default void remove(UniversityComponent c) { throw new UnsupportedOperationException(); }
}
class Department implements UniversityComponent {
private String name;
private List<UniversityComponent> children = new ArrayList<>();
public void sendNotification(String message) {
System.out.println("向部门 " + name + " 发送通知: " + message);
for (UniversityComponent child : children) {
child.sendNotification(message); // 递归通知所有子节点
}
}
public void add(UniversityComponent c) { children.add(c); }
public void remove(UniversityComponent c) { children.remove(c); }
}
class Person implements UniversityComponent {
private String name;
private String role; // "Student" or "Teacher"
public void sendNotification(String message) {
System.out.println("向" + role + " " + name + " 发送通知: " + message);
}
}
参考课件:
slides/07适配器与组合.pdf(组合模式)
12. 什么是组合命令(又称宏命令)?写出大概的组合命令类的代码。¶
组合命令(宏命令/Macro Command)定义¶
组合命令(Macro Command) 是命令模式的一种扩展,它将多个命令对象组合成一个复合命令。当执行这个宏命令时,会依次执行它所包含的所有子命令。
组合命令本身也是 Command 接口的实现,因此可以像普通命令一样被调用、排队和撤销。
代码¶
// 命令接口
interface Command {
void execute();
void undo();
}
// 宏命令(组合命令)
class MacroCommand implements Command {
private List<Command> commands = new ArrayList<>();
public void add(Command cmd) {
commands.add(cmd);
}
public void remove(Command cmd) {
commands.remove(cmd);
}
@Override
public void execute() {
for (Command cmd : commands) {
cmd.execute(); // 依次执行每个子命令
}
}
@Override
public void undo() {
// 逆序撤销(后执行的先撤销)
for (int i = commands.size() - 1; i >= 0; i--) {
commands.get(i).undo();
}
}
}
// 使用示例
MacroCommand macroCmd = new MacroCommand();
macroCmd.add(new LightOnCommand(light));
macroCmd.add(new MusicPlayCommand(music));
macroCmd.add(new TVOnCommand(tv));
macroCmd.execute(); // 一键执行全部操作
参考课件:
slides/05状态与命令模式.pdf(命令模式 / 宏命令)
13. 设计一个基于管道-过滤器模式的、输入文本文件、输出排序后的无重复的单词列表的程序。¶
13.1 组件部分和连接器画出程序图¶
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Read │ │ Split │ │ Lower │ │ Sort │ │ Write │
│ File │───▶│ Words │───▶│ Case │───▶│ Unique │───▶│ Output │
│(读取文件)│ │(分词) │ │(转小写) │ │(排序去重)│ │(写输出) │
└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
文本流 ────────▶ 单词流 ────────▶ 小写词流 ──────▶ 有序集合 ──────▶ 输出文件
(String) (String[]) (String[]) (SortedSet) (File)
其中 →(箭头) 是连接器(管道/Pipe),方框是过滤器(Filter)组件。
13.2 组件的类图¶
┌──────────────────┐
│ <<interface>> │
│ Filter │
├──────────────────┤
│ + process(input): │
│ Object │
└────────┬─────────┘
△
┌────┴───────────────┐
│ │
┌───┴──────────┐ ┌──────┴───────┐ ┌────────────────┐
│ FileReader │ │ WordSplitter│ │ SortUnique │
│ Filter │ │ Filter │ │ Filter │
├──────────────┤ ├──────────────┤ ├────────────────┤
│+ process(): │ │+ process(): │ │+ process(): │
│ String │ │ String[] │ │ SortedSet │
└──────────────┘ └──────────────┘ └────────────────┘
┌──────────────────┐ ┌──────────────────┐
│ LowerCaseFilter │ │ FileWriterFilter│
├──────────────────┤ ├──────────────────┤
│+ process(): │ │+ process(): void │
│ String[] │ │ (writes to file)│
└──────────────────┘ └──────────────────┘
┌─────────────────────────────┐
│ Pipeline │
├─────────────────────────────┤
│ - filters: List<Filter> │
│ - pipes: List<Pipe> │
├─────────────────────────────┤
│ + addFilter(f: Filter) │
│ + execute(): void │
└─────────────────────────────┘
13.3 Java 实现¶
import java.io.*;
import java.util.*;
// 过滤器接口
interface Filter {
Object process(Object input);
}
// 管道(连接器)
class Pipe {
private Object data;
public void put(Object data) { this.data = data; }
public Object get() { return data; }
}
// 管道线
class Pipeline {
private List<Filter> filters = new ArrayList<>();
public void addFilter(Filter f) { filters.add(f); }
public void execute(Object input) {
Object data = input;
for (Filter filter : filters) {
data = filter.process(data);
}
}
}
// Filter 1: 读取文件
class FileReaderFilter implements Filter {
public Object process(Object input) {
String filename = (String) input;
StringBuilder sb = new StringBuilder();
try (BufferedReader br = new BufferedReader(new FileReader(filename))) {
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append(" ");
}
} catch (IOException e) { e.printStackTrace(); }
return sb.toString();
}
}
// Filter 2: 分词
class WordSplitterFilter implements Filter {
public Object process(Object input) {
String text = (String) input;
return text.split("\\W+"); // 按非单词字符分割
}
}
// Filter 3: 转小写
class LowerCaseFilter implements Filter {
public Object process(Object input) {
String[] words = (String[]) input;
List<String> result = new ArrayList<>();
for (String w : words) {
if (!w.isEmpty()) result.add(w.toLowerCase());
}
return result.toArray(new String[0]);
}
}
// Filter 4: 排序去重
class SortUniqueFilter implements Filter {
public Object process(Object input) {
String[] words = (String[]) input;
SortedSet<String> sorted = new TreeSet<>(Arrays.asList(words));
return sorted;
}
}
// Filter 5: 写入文件
class FileWriterFilter implements Filter {
public Object process(Object input) {
@SuppressWarnings("unchecked")
SortedSet<String> words = (SortedSet<String>) input;
try (PrintWriter pw = new PrintWriter("output.txt")) {
for (String w : words) {
pw.println(w);
}
} catch (IOException e) { e.printStackTrace(); }
return null;
}
}
// 主程序
public class PipeFilterDemo {
public static void main(String[] args) {
Pipeline pipeline = new Pipeline();
pipeline.addFilter(new FileReaderFilter());
pipeline.addFilter(new WordSplitterFilter());
pipeline.addFilter(new LowerCaseFilter());
pipeline.addFilter(new SortUniqueFilter());
pipeline.addFilter(new FileWriterFilter());
pipeline.execute("input.txt");
}
}
13.4 代码与图的映射关系¶
| 图中的组件 | 代码中的类 | 说明 |
|---|---|---|
| Read File(读取文件) | FileReaderFilter |
读取输入文本文件,返回文本内容字符串 |
| Split Words(分词) | WordSplitterFilter |
将文本按非单词字符分割为单词数组 |
| Lower Case(转小写) | LowerCaseFilter |
将所有单词转为小写 |
| Sort Unique(排序去重) | SortUniqueFilter |
使用 SortedSet/TreeSet 实现排序和去重 |
| Write Output(写输出) | FileWriterFilter |
将结果写入输出文件 |
| 连接器 → | Pipe 类 / 方法调用 |
通过 Pipeline.execute() 中 Object data 在各 Filter 间传递数据 |
参考课件:
slides/软件架构模式_update_2.pdf(管道-过滤器模式),slides/2026SUG_SysArch1_introduction.pdf(C&C 风格)
综合参考:
exams/软件系统设计-复习-EagleBear.pdf(综合复习资料)