参考答案

我们无法保证答案的正确性,如果你对参考答案有任何疑问,请联系助教! 类图答案不唯一,能表示出类之间关系与设计模式即可。

创建型模式

STEP1

Alice 的需求是:屏蔽智能指针创建 vector 的细节,让用户不用关心 make_unique、make_shared 等具体写法,同时未来可能增加新的智能指针类型。

适合使用工厂方法模式设计。

选择原因:

  • 创建对象的逻辑被封装在工厂类中,Alice 不需要记住 std::make_unique<std::vector<int>>() 这种具体写法。每一种智能指针对应一个具体工厂,职责清晰。
  • 由于这里创建的是“某一种产品”的不同变体,也就是“由不同智能指针包装的 vector”,所以用工厂方法模式比抽象工厂模式更合适。

类图:

STEP2

Alice 的第二个需求是:根据不同应用场景,同时提供一组相关容器:一个顺序容器和一个关联容器。

适合使用抽象工厂模式设计。

选择原因:

  • 每个应用场景都对应一组容器组合,例如 vector + map、vector + unordered_map、deque + map。用户只需要选择场景,不需要自己纠结具体容器类型。抽象工厂可以保证同一场景下创建出来的顺序容器和关联容器是匹配的一组产品。
  • 未来如果增加新的场景,例如“频繁中间插入场景:list + map”,只需要增加新的具体工厂,不需要修改已有工厂接口,扩展性较好。

类图:

创建型模式总结

  • STEP1 适合工厂方法模式:解决“创建某一种产品的不同具体实现”。
  • STEP2 适合抽象工厂模式:解决“创建一组相关产品族”的问题。

结构型模式

STEP1

Bob 想要的不是重新实现一个真正的向量,而是把 std::vector 的接口“翻译”成自己喜欢的接口,比如把 push_back() 改成 push(),把 size() 改成 len()

所以这里适合使用适配器模式设计。

std::vector 是已有类,功能强大但接口不符合 Bob 的使用习惯;BobVector 则作为适配器,对外提供 Bob 想要的接口,对内复用 std::vector 的功能。

选择原因: Bob 的核心需求是“接口转换”,不是增加功能,也不是隐藏一组复杂子系统。BobVectorstd::vector 原本的接口转换成 Bob 更容易记忆和使用的接口,因此适配器模式最合适。

类图:

STEP2

Bob 已经有了多个负责不同功能的小类:BobVectorSorter 负责排序,BobVectorDeduplicator 负责去重,BobVectorPrinter 负责打印。问题不在于这些类不能用,而是用户每次都要记住调用顺序和调用细节。

这里适合使用外观模式设计。

BobVectorServant 就是一个外观类。它对外提供一个简单的高级接口,比如 tidy_and_print(),内部负责协调“判空、排序、去重、打印”的完整流程。

选择原因: 外观模式的重点是为复杂子系统提供一个统一、简单的入口。用户不需要直接依赖 sorter、deduplicator、printer,也不需要知道它们的协作顺序,只需要调用 BobVectorServant 的高级接口即可。

类图:

STEP3

这里适合使用代理模式

Bob 不想直接修改原来的 BobVector,但又想提供一个接口完全相同、只是在 get() 时多做越界检查的新类。这很符合代理模式的思想:代理类和真实对象具有相同接口,代理类在访问真实对象前后增加一些控制逻辑。

这里的新向量类可以看作 BobVector 的保护代理。它在调用真正的 get() 之前先判断下标是否合法,如果合法再转发给原对象,如果不合法就抛出异常或进行错误处理。

选择原因: 这个需求的重点不是添加或更改功能,而是“在不改原类的前提下,对访问行为进行控制”。越界检查本质上是一种访问保护,所以代理模式比适配器模式更贴切。

类图:

STEP4

Bob 希望可以给同一个 BobVector 自由添加不同功能,而且这些功能可以组合,甚至同一个功能可以添加多次。比如一个向量可以同时拥有“宾邦功能”和“虚假繁荣功能”,也可以连续套两层“宾邦功能”。

这里适合使用装饰器模式

装饰器模式正是用来动态地给对象添加职责的,它适用于类别功能之间独立,但可以自由累次组合的情况。

选择原因: 装饰器模式可以在不修改 BobVector 的情况下扩展功能。每个功能都可以做成一个具体装饰器,并且装饰器之间可以一层一层包装。因此它非常适合这种“功能可选、可组合、可重复添加、未来还可能继续扩展”的场景。

类图:

结构型模式总结

  • STEP1 采用的适配器模式的意图是转换接口,让原本接口不兼容的类可以被使用。它适合用于复用旧代码、第三方库,或者把已有类包装成更符合当前系统需求的接口。
  • STEP2 采用的外观模式的意图是为复杂子系统提供一个简单统一的入口。它适合内部类很多、调用流程复杂,但用户只需要使用少量高级功能的场景。
  • STEP3 代理模式的意图是为真实对象提供一个替身,并在访问真实对象时加入控制逻辑,接口不会改变。它适合权限检查、越界检查、延迟加载、缓存、远程访问等场景。
  • STEP4 采用的装饰器模式的意图是在不修改原类的情况下,动态地给对象增加新功能。它适合功能可以自由组合、按需添加,并且未来扩展种类较多的场景。

行为型模式

STEP1

Carol 想让 sort() 在运行时可以选择不同排序算法,比如冒泡排序、插入排序、快速排序、希尔排序等。排序这件事的目标是一样的,都是把 CarolVector 从小到大排序,但具体算法可以自由替换。

这里适合使用策略模式进行设计。

设计时可以把排序算法抽象成统一的 SortStrategy,不同算法分别实现为 BubbleSortStrategyQuickSortStrategyShellSortStrategy 等。CarolVector::sort() 接收或持有一个具体策略对象,然后把排序工作交给它完成。

选择原因: 策略模式适合处理“同一任务有多种算法实现,并且需要在运行时选择”的场景,且通过委派来实现复用。以后 Carol 想增加新的排序算法时,只需要新增一个策略类,不需要修改原有排序逻辑,扩展性较好。

类图:

STEP2

Carol 的不同 print() 方法虽然格式不同,但整体流程其实是固定的:先输出前缀,再依次输出元素,中间输出分隔符,最后输出后缀。真正变化的只是前缀、后缀、分隔符这些局部细节。

这里适合使用模板方法模式设计。

可以设计一个抽象的打印模板类,在父类中定义完整的打印流程,而把“获取前缀”“获取分隔符”“获取后缀”这些步骤交给子类实现。比如方括号格式、花括号格式、短横线格式、特殊装饰格式都可以作为不同子类。

选择原因: 模板方法模式适合“算法整体流程固定,但某些步骤需要由子类定制,且步骤在创建时固定”的场景,且通过继承来实现复用。这里各种打印格式不应该重复写整段循环代码,只需要复用打印流程,并修改有差异的部分。 需要注意的是,这个需求也有一点策略模式的味道,因为 Carol 希望创建时选择打印方式。但重点在于“创建时固定选择内容,而非动态挑选步骤”,所以模板方法模式更贴切。

类图:

STEP3

Carol 希望当一个向量执行插入操作时,其他指定的向量可以自动执行相同插入操作,而且这些同步对象可以动态增删。

这里适合使用观察者模式设计。

可以把被操作的向量看作主题对象,其他需要同步插入的向量看作观察者。当主题对象发生 push() 操作时,它通知所有已注册的观察者,观察者收到通知后执行同样的插入。

选择原因: 观察者模式适合“一对多通知,且通知对象需要动态变化”的场景。一个对象状态或行为发生变化后,多个依赖它的对象可以自动响应。同时,观察者可以动态注册和移除,正好符合 Carol 希望动态控制同步对象的需求。

类图:

STEP4

Carol 想给向量增加很多新操作,比如求和、求积、筛选、整体加常量等,但她不想每增加一个操作就修改 CarolVector 的接口,也不想把大量小众功能都塞进向量类本身。

这里适合使用访问者模式设计。

可以让 CarolVector 提供一个统一的 accept(visitor) 接口,然后把不同操作封装成不同访问者,例如 SumVisitorProductVisitorAddConstantVisitor 等。向量只负责接收访问者并让访问者访问内部元素,具体操作由访问者完成。

选择原因: 访问者模式适合“对象结构比较稳定,但需要频繁增加不同的新操作”的场景。CarolVector 的基本结构是稳定的,变化的是对元素执行的操作,因此把操作移到访问者类中,可以避免频繁修改向量类本身。

类图:

STEP5

CarolVector 在不同状态下,对同一个操作会有不同反应。比如空状态下 pop() 会报错,冻结状态下 push() 会被拒绝,普通状态下 pop() 可以正常执行,追加状态下只能 push()

如果把这些判断都写在成员函数里,就会出现大量 if-else,而且新增状态时要修改很多旧代码。

这里适合使用状态模式设计。

状态模式可以把不同状态封装成不同状态类,例如 EmptyStateNormalStateFrozenState 当前持有一个状态对象,调用操作时把请求交给当前状态处理。状态对象还可以在合适的时候切换 CarolVector 的状态。

选择原因: 状态模式适合“对象行为依赖当前状态,并且行为会和导致状态转换”的场景。它可以把复杂状态判断从 CarolVector 中拆出去,使每个状态的行为更清晰,也方便以后增加新的状态。

类图:

行为型模式总结

  • STEP1 采用的策略模式的意图是把一组可替换的算法封装起来,让用户可以在运行时选择具体算法。它适合排序方式、路径规划、压缩算法、支付方式等“目标相同但实现不同”的场景。
  • STEP2 采用的模板方法模式的意图是在父类中固定算法骨架,把某些变化步骤延迟到子类实现,在创建时确定。它适合流程稳定、细节可变的场景,比如打印格式、文件导出流程、数据处理流程等。
  • STEP3 采用的观察者模式的意图是建立一种一对多的通知机制,且被通知对象需要动态变化。当一个对象发生变化时,依赖它的多个对象可以自动收到通知并更新。它适合事件监听、消息订阅、界面刷新、数据同步等场景。
  • STEP4 采用的访问者模式的意图是把作用于对象结构上的操作独立出来,使新增操作时不必频繁修改对象本身。它适合对象结构稳定、操作种类经常增加的场景。
  • STEP5 采用的状态模式的意图是把对象在不同状态下的不同行为分别封装起来,让对象行为随着内部状态改变而改变。它适合状态较多、状态转换明确、不同状态下行为差异明显的场景。
Last Updated:
Contributors: 1-rambo