跳转至

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)适用环境

使用命令模式的情况:

  1. 需要将请求封装为对象:在不同的时刻指定、排列和执行请求
  2. 需要支持撤销操作(Undo):Command 对象可以存储状态以支持撤销。Command 接口通常需要定义 execute()undo() 方法
  3. 需要支持日志和事务:将操作记录为日志,在系统崩溃时可以重新执行
  4. 需要支持宏命令(Macro Command):将多个命令组合成一个复合命令
  5. 需要将调用操作的对象与知道如何实现该操作的对象解耦:Invoker 不直接知道 Receiver,通过 Command 对象间接调用
  6. 需要在不同时间指定、排队和执行请求:Command 对象的生命周期可以独立于原始请求

命令模式结构

┌──────────┐     ┌─────────────┐     ┌──────────┐
│  Invoker │────▶│ <<interface>>│◀────│  Client  │
│(调用者) │     │   Command   │     │(客户端) │
└──────────┘     ├─────────────┤     └──────────┘
                 │+ execute()  │
                 │+ undo()     │
                 └──────┬──────┘
                 ┌──────┴──────┐
                 │ConcreteCmd  │
                 ├─────────────┤
                 │- receiver   │────▶┌──────────┐
                 │+ execute()  │     │ Receiver │
                 │+ undo()     │     │(接收者) │
                 └─────────────┘     └──────────┘

参考课件slides/05状态与命令模式.pdf(命令模式)


5. 隔栏和断言的目的是什么?

"隔栏"即隔离程序(Barrier),"断言"即Assertion,都属于防御式编程的范畴。

防御式编程的核心思想

可以预见到(至少预先推测到)问题所在,断定代码中每个阶段可能出现的错误,并作出相应的防范措施,来防止类似意外的发生。

断言(Assertion)的目的

  1. 开发期间使用,让程序在运行时进行自检
  2. 是对开发人员的警告,通常是一个子程序或宏
  3. 断言不可以有副作用——不能影响程序的正常逻辑
  4. 帮助开发人员在开发阶段尽早发现程序中的逻辑错误

隔离程序(Barrier/隔栏)的目的

  1. 以防御式编程为目的而进行隔离
  2. 将某些接口选定为 "安全"区域的边界
  3. 对穿越安全区域边界的数据进行合法性检验
  4. 将校验工作集中在特定的模块中以降低成本
  5. 防止错误数据从一个模块传播到另一个模块

断言 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.pdfslides/Lecture 01 - Attributes Driven Design.pdf


7. 为什么设计决定对架构很重要?说出你对"软件架构=设计(如,组件-连接器)+设计决定"的理解。

为什么设计决定很重要

  1. 设计决定(Design Decisions)是架构的核心——架构不仅是系统的结构,更是一组有意识的选择
  2. 设计决定记录了为什么选择某种结构,而不仅仅是描述结构本身
  3. 设计决定包含了权衡(trade-offs)——为什么选择 A 而不是 B
  4. 设计决定使架构可以被评估、追溯和演化

对"软件架构 = 设计(组件-连接器)+ 设计决定"的理解

软件架构 = 结构(Structure)+ 设计决定(Design Decisions)
         = 是什么(What) +  为什么(Why)
  • 组件-连接器(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(综合复习资料)