行为型模式
在本节作业中,你将主要关注策略模式、模板方法模式、观察者模式、访问者模式、状态模式这五种行为型设计模式。
在本节你一共需要绘制两张类图,请任选两个STEP,在选择设计模式后额外画出类图。
在完成所有STEP的任务之后,你还需要用自己的语言简单总结上述五种设计模式的意图和应用场景,并和STEP的解答一同放在最终提交的PDF中。
或许在正式开始作答前,先去看看TIPS章节会对你有所帮助。
STEP1
Carol 看到Bob包装了自己的向量类,并尽情地实现了一系列功能,非常眼红,于是也创造了自己的CarolVector类,她初步实现了和BobVector一样的接口:
void push(Elem):在向量末端插入一个新元素Elem pop():移除向量末端的元素并将其返回size_t len():返回向量中的元素个数bool is_empty():如果向量为空,返回true,否则返回falseElem& get(size_t):返回对应位置元素的引用
然后,她想为CarolVector添加一个新的成员函数sort()(注意形参未标出),用于从小到大排序向量中的全部元素。Carol 学过很多排序算法,包括单不限于冒泡排序,插入排序,快速排序,希尔排序等,Carol 患有严重的选择困难症,她不知道该用哪种排序算法实现这个函数,于是打算干脆全实现了,让用户在调用sort()方法时再动态选择用哪个算法进行排序。Carol 未来还计划实现更多的排序算法实现,所以可拓展性是必要的。
她该用哪个设计模式在实现这个需求?
STEP2
Carol 为CarolVector实现了print()方法,用于输出向量中的每个元素,她的实现可能长这样:
// 输出格式示例:1 2 3 4 5
void CarolVector::print() {
auto len = self.len();
for (size_t i = 0; i < len; ++i) {
std::cout << self.get(i);
if (i != len - 1) { // 最后一个元素后不加分隔符
std::cout << ' ';
}
}
}
在完成这个print()的实现后,她又想实现更多的输出格式,这些输出格式五花八门,但总结下来无非是更改元素间的分割符,和为输出内容增加前缀和后缀。以下是几种新的输出格式示例:
/// [1, 2, 3, 4, 5]
/// {1, 2, 3, 4, 5}
/// 1-2-3-4-5
/// ꧁༺ 卍 1✧2✧3✧4✧5 卍 ༻꧂
Carol 不希望同一个CarolVector支持多种print()方法,太乱了,她希望在创建时就决定这个CarolVector用哪种print()方法。同时,在实现这些不同的print()方法时,她希望尽可能地复用已经写好的代码,只实现有区别的部分,以避免自己的项目里出现大段重复的代码。当然,Carol 会不断地产出新的print()方法,可拓展性也是必要的。
她该用哪个设计模式在实现这个需求?
STEP3
Carol 在使用自己的向量类时,注意到了一个新的需求。她发现她自己经常需要对固定的几个向量类对象进行相同的插入操作,而每次编写这样的操作都需要写一个循环,非常麻烦。Carol 想,如果能做到,每当对其中一个对象进行插入操作时,其他对象能自动地也执行这样的插入操作,那就好了。当然,得能动态地控制哪些对象会自动地复刻插入操作。
有没有哪个的设计模式能满足这样的需要呢?
STEP4
Carol 想为自己的向量类添加一系列新的操作,获取所有元素的和,获取所有元素的积,筛选满足条件的元素子集,为所有元素都增加一个常量值,等等。最重要的是,她还没想好一共要添加多少新的操作,也就是说她以后可能随时会想出新的操作并添加进来。但是她并不想频繁地改动向量类的接口,也即每次添加新操作都不会增加或减少向量类本身的成员函数。也不想让这么多操作都强行绑定在向量类中(毕竟有些操作太小众了可能用户一辈子都用不到)。
这看起来有点不可能,但是是否恰好有那么一个设计模式能满足这样的需求呢?
STEP5
Carol 在使用自己的向量类时,发现 CarolVector 在不同情况下似乎应该表现得不太一样。
比如,一个刚创建出来的空向量,调用 pop() 时当然不应该真的弹出元素;而当它第一次成功 push() 之后,它又应该变成一个可以正常 pop() 和 get() 的普通向量。除此之外,Carol 还想给向量增加一些特殊的运行状态:
- 空状态:向量中没有元素,调用
pop()或get()时会给出错误提示。 - 普通状态:向量可以正常
push()、pop()、get()、sort()。 - 冻结状态:向量内容暂时不允许被修改,调用
push()、pop()、sort()时都会被拒绝,但仍然可以get()和print()。 - 追加状态:向量只能在末尾继续
push()新元素,不能pop(),也不能修改已有元素。
Carol 当然可以在每个成员函数里都写一大堆 if 来判断当前到底是哪种状态,但是这样一来,push()、pop()、get()、sort() 等函数里都会混进很多状态判断代码。更可怕的是,如果以后 Carol 又想增加新的状态,这些老函数可能又要被翻出来改一遍(想想就可怕!)。
Carol 希望把不同状态下的行为分别封装起来,让 CarolVector 根据自己当前所处的状态,自动决定调用哪一套处理逻辑。同时,某些操作还可能改变向量的状态,例如空向量成功插入元素后进入普通状态,普通向量弹出最后一个元素后重新进入空状态。
有没有哪个设计模式能满足这样的需要呢?
TIPS
- 在使用状态模式时,如果需要同时管理多个相互独立的状态,你很可能需要设计多个状态类分别处理。如果不仅有多个状态,而且这些状态之间还有复杂的相互影响的关系,那状态模式可能就因为复杂度过高而不再适用了。
- 注意区分访问者模式和迭代器模式(虽然我们没讲但你应该完全能理解这是个什么东西),虽然这两者经常被协同使用,但是其侧重点完全不一样,不要把迭代器模式当成访问者模式的一部分甚至全部了!
- C++ 模板并不是模板方法模式的一个实例!两者的根本性区别在于, C++ 模板实现的是编译时多态,而模板方法模式实现的是运行时多态。当然它们都叫作模板肯定是有共同点的,比如说它俩都可以减少项目中的重复代码。