
是设计模式!
本篇对2021年秋季学期的《软件架构与设计模式》课程进行整理回顾析,如有出入还请以侯捷老师为准。
UML的一些补充

继承:在继承时,不能根据语言的字面来进行判断。例如鲸鱼不是鱼(是哺乳动物);而鸵鸟虽然是鸟,但却不会飞,因此要将鸟这个父类分为会飞的鸟和不会飞的鸟。
组成Composition:实心菱形,代表存储的是实体。例如一辆车子有四个轮子,汽车销毁,轮子也一起销毁,它们的生命周期相同,那就是一种组成的关系。
聚合Aggregation:空心菱形,代表存储的是指针。例如一辆车子有四个轮子,汽车销毁,轮子不会销毁,那就是一种聚合的关系。
Strategy


这是一个小型的排版软件例子,效果在右侧。
main函数在循环询问用户使用什么样的排版方式/指定最大宽度,实时改变使用的Strategy。
main函数里面test对象类型是TestBed,在设置了strategy之后,会调用test.doIt()函数,里面执行了strategy_->format()这一句指令。format()这个函数在每个Strategy子类当中进行具体化定义。
20行strategy_这个变量指向Strategy,它的默认值是null,用户通过调用setStrategy()这个函数来为strategy_这个指针实例化一个Strategy的子类。

format()函数执行的是排版过程,每次从文件中读取一个词,然后进行处理,超出最大宽度之后通过justify()函数进行排版。
Strategy模式适用于对同一个问题的不同处理办法,如果后续增加新的需求,那么可以通过增加子类的方式就能实现。尽量把改动集中在UI的修改上。
此例中,setStrategy是通过if-else来书写的,当种类较多时这么写死并不好,可以考虑改用Factory Method或Prototype来实现。
OCP:对增加功能是开放的,对修改代码是封闭的。
Composite
将objects复合成为树状结构(不是数据结构中严格意义上的树)来表现“局部-全部”体系结构。Composite让clients得以一致的方式对待个别物和合成物。
常见例子:文件系统、窗口系统、Command Pattern建立macro commands。

1. 右边Directory必须有一个容器,因此需要设计一个父类Entry。
2. 因为是容器vector,所以要有对元素的操作。因为要操作左边和右边,所以传入的参数类型为父类。
3. 左边File的getSize函数就返回文件的大小,右边Directory的getSize就是通过迭代器迭代容器之后,累加大小。这里取出每一个元素没有去判断它的类型,“以一致的方式对待”。可以这样“递归”是Composite的一种特性。
1.add()函数为什么出现在Enrty中?
这里的add()函数只能出现在Directory中,不能出现在File中,因为不符合逻辑。
但如果只在Directory中出现而不放到父类Entry中,从容器里拿到一个元素时,程序必须判断到底是File还是Directory以便决定是否可以调用add(),导致不能对它们一视同仁。
因此虽然对于Entry来说并不需要这个函数,但还是干脆把add()抽到父类中,让它默认抛出异常(这样如果调用File.add()就会抛出异常)。
2.上述结构能写到框架端吗?
File和Directory是应用端,好像唯一能写到框架端的只有Entry,但里面要做的事情完全跟它的子类有关,所以不适合抽到框架端。这个设计模式没有一个地方是适合放在框架端的。
3.设计父类时,要把子类的最小共通函数抽取出来,使得容器和被容物能被一致使用。

这部分实例源码可以在右上角的书上下载到,链接放在文末。捷哥建议大家尝试将Java的代码转成C++进行练习。
Flyweight

享元,共享的元素,经过良好设计后粒度已经细到适合共享。
如果要用到某种对象,而且是一样的,那么我们用共享的方式比较好。
要好好分析什么东西是可以共享的,什么东西是不可以共享的。对一篇英文文稿而言,flyweight是“字符”,每个字符的state只需是ascii code + color + size + position。能够共享的就是控制点,颜色、坐标、大小都不能共享。这一点至关重要。

从右下角的Main开始,以程序执行的方式输入,生成BigString。new BigString时调用构造函数。
如果new BigChar数组,则会根据输入循环从硬盘中读出BigChar对应的图片。但每次都从硬盘重复读取速度就会很慢,因此引入Factory Method,希望能快速从容器中读取到已读过的字符。
因为只需要一个工厂,设计成Singleton即可(让外界通过getInstance来获得)。
同时要用一种查找起来尽可能快的数据结构作为容器(C++中可以选哈希表和红黑树)。
1.此处哈希表的键为什么为string而不是char?
其实可以是char的,但Java Collection不接受primitive type,因此作者使用了string。
2.如何体现Singleton?
此处成员bcf和getInstance的下划线代表static,是静态成员。同时构造函数是private,那么外界无法构造它,只能通过public接口获得。
getBigChar提供获取字符的接口,在容器中找到直接返回,没有则直接new后从硬盘中读取放入池中再返回。
补充内容
关于Singleton的禁绝法,捷哥做了一些补充。

右边的写法:一般来说把“静态的自己”当作成员变量,但这里使用了静态函数来处理静态变量让外界来调用,外界不需要任何Printer实例就可以调用。把静态变量放在函数中的好处是,只有当外界调用函数的时候,才会出现这个变量,没有人调用时就不占据内存。注意要把凡是会产生拷贝的函数都放入private中。
左边的写法:“静态的自己”通过new产生。当外界调用instance时,它会new然后返回指针。
clients调用时,使用Printer::Instance()即可。


左边是C++版本,右边是Java版本。
左边写的FontData和右边的BigChar是一个东西。

因为希望让程序变得更弹性,因此在main中循环可以让用户多次输入。
但是问题出现了,第0次输入了13463,容器中会放入{1,3,4,6},可以看到遇到第二个3时,没有再次读入。但接下来工厂的析构函数被调用了。
下一次循环时,工厂中没有之前读入的数字,又重新读取了。发现每一次容器都会被清空。
这里调用了一次ctor,但是调用了四次dtor,但其实Singleton的生命应该延续到程序结束为止。
多出的三次dtor是因为每次循环后BigString的生命周期结束,可以看到13页BigStirng在内部使用getInstance调用了工厂(的引用)。
FontDataFactory factory = FontDataFactory::getInstance();
但每次的析构函数都会把factory这个对象析构掉。
方法一:是因为FontDataFactory里面声明的容器不是静态的,只需要将其修改为static就不会被析构函数释放掉。
补充说明,静态成员变量必须在类声明的外部初始化,修改如下。

方法二:删除BigString67行的拷贝复制语句,在工厂类的private中添加拷贝函数。
注意是如果程序员不声明拷贝函数,那么C++默认为类生成,现在写入private中也就禁止了其他类的拷贝行为。

但是如果不对BigString中拷贝工厂那一句进行修改,像如下只修改getInstance获得的形式,这样不起作用。
如下是上述提到在getInstance调用时才会new一个FontDataFactory,然后返回指针,这样做的好处也说了,在不调用的时候不会占据内存。
运行之后发现在进入下一个循环之前仍然会调用析构函数,因此出现这个问题不是由Singleton是通过static object还是static pointer实现这一点引起的,而是由client拷贝了工厂生成新的实例引起的。

如下修改在方法二的基础上将赋值运算也放入private中,进一步禁止拷贝,也可以解决问题。

写在最后
有段时间没回来复盘设计模式了!
去学了一圈数据结构、操作系统和计算机网络,在写题的时候熟悉了C++语法,颇有感触。这个时候再结合捷哥课上的细节,会感觉还是很有收获。
Java代码的源码下载链接:
www.hyuki.com/dp/dpsrc_2004-05-26.zip
文案:咸
排版:咸




