暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

设计模式笔记 | Strategy & Composite & Flyweight

IT双侠的咕咕咕日常 2022-09-23
460

是设计模式!


本篇对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


文案:咸

排版:咸


文章转载自IT双侠的咕咕咕日常,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论