设计模式学习
如何解决一个复杂的问题:分解、抽象。
分解是将大问题变为小问题;抽象是将问题的共同点提炼出来,而后对不同点做不同处理。
抽象比分解更好
软件设计的目的是复用
1.设计原则
高层模块(稳定)不能依赖于底层模块(变化),二者应该依赖于抽象(稳定)
抽象(稳定)不应该依赖于实现细节(变化),实现细节(变化)应该依赖于抽象(稳定)
对扩展开放,对修改封闭
一个类应该仅有一个引起它变化的原因,单一职责
接口应该小而完备,该private就private,不能随便public
优先使用对象组合,而不是类继承
面向接口(对象要声明成抽象基类),高内聚,松耦合。
稳定存在于编译时,变化存在于运行时,用虚函数将编译时依赖延迟到运行时依赖
变化无法消除,关键在于让变化只存在于代码的某个集中区域而不是充斥在整个代码空间
GOF-23分类:创建型、结构型、行为型
组件协作模式:模板方法、策略模式、观察者模式
单一职责模式:装饰模式、桥模式
对象创建模式:工厂方法模式
2.模板方法(Template Method):
晚绑定与松耦合
晚绑定:早的东西调用晚的东西,先定义一个算法的骨架,而将步骤实现延迟到子类中
让子类变化基类的函数,使子类复用一个稳定算法结构而重写其中的特定步骤
程序必须有一个稳定的算法骨架才能使用模板方法
3.策略模式(Strategy):
以扩展的方式支持未来的变化,降低代码的冗余
使用基类与子类的关系扩展代码,保证调用段的稳定(多态调用)
用于重构switch-case和if-else代码(从分解进化为抽象)
对于绝对不变的系统,可以使用switch-case和if-else,否则就推荐重构为策略模式
观察者模式/事件模式(Observer/Event):以一个抽象通知机制影响多个观察者
定义对象间的一对多依赖关系,称一为目标,多为观察者。
当目标的状态发生改变时,无需指定观察者,通知就会自动传播,所有依赖于目标的观察者都会得到通知并由观察者自身决定是否需要订阅通知以产生更新,而目标对此一无所知。
不推荐多继承,要使用多继承应当为一个主基类和多个接口的形式
很适合用于UI事件的构造
下例中目标为已传输文件的数目,观察者分别为进度条显示和字符“.”显示。
如果不使用观察者模式,每次用户提出新的显示需求我们都需要更改高层模块新增具体通知控件,不符合设计原则。定义进度接口IProgress内含虚函数DoProgress以供不同的显示方式复用,定义观察者列表m_iprogressList以支持多个观察者。当已传输文件的数目value变化时,遍历观察者列表,通过调用OnProgress函数将这个变化通知利用各观察者的DoProgress传播到所有需要的观察者中并作出相应的显示更新。DoProgress在各显示方法对应的类中实现。
4.装饰模式(Decorator):
附着在其他基本功能类之上扩展功能类。如果对每个基本功能类都各自去产生继承的扩展功能类,会产生大量的冗余代码。正确做法是将各个扩展功能独自成为扩展功能类,而在有需求时去对其他基本功能类进行装饰,体现单一职责原则。
程序中存在大量可以去除的重复代码
当一个变量的声明类型都是某个类型的子类时,把该变量直接声明成该类型即可
用编译时装配替代运行时装配
明辨类何时应该继承,当各类属于一个变化方向时才应该使用继承
下例中,为了实现对不同流(File文件流,Net网络流,Memory内存流)的不同操作(无操作,Crypto加密操作,Buffered缓冲操作,CryptoBuffered缓冲加密操作),一般方法是对基本功能类做原类的继承,再把扩展功能类做基本功能类的继承,这种继承模式实际上是对继承的过度调用,将各操作继承于各流,两者不属于一个变化方向,这么做很容易使问题规模无限扩大。装饰模式指出应该将基本功能类和扩展功能类分开继承,定义n个基本功能类,m个扩展功能类继承自装饰模式类,而后装饰模式类和基本功能类共同继承自原类,故总共只需要m+n+2个类,使问题规模大大降低。这种模式的结构是相对固定的,因为扩展功能类的传入对象必然是各基本功能类,因而两者必然继承自同一原类。
5.桥模式(Bridge):
应对多维度的变化。它与装饰模式类似,区别在于它具有多个变化维度,需要用多个稳定原类,使得各变化可以沿着他们各自的维度去变化。
下例为一个消息传输程序,它既有PC端和手机端的分别需求,又有精简版Lite和完全版Perfect的分别需求,其中PC端和手机端的PlaySound,DrawShape,WriteText,Connect函数不同,而完全版Perfect在精简版Lite的Login,SendMessage,SendPicture基础上添加了PlaySound。对比装饰模式,可以看到Messager和MessagerImp之间没有明显的基本与扩展关系,两者是不同维度的并行变化关系,此时就要使用桥模式。PCMessagerImp和MobileMessagerImp实现MessagerImp,MessagerLite和MessagerPerfect实现Messager,实现桥构造。
运行端以PC为例:
6.工厂方法(Factory Method):
通过一种对象创建的模式来绕开堆创建new和栈创建所带来的紧耦合(依赖具体类),从而支持对象创建的稳定,同时满足将对象创建的变化集中在某个区域的效果。
我们先来看看常见的对象创建方法:
以上两个创建方法分别为堆创建和栈创建,可以看到他们都依赖于了具体的类,是不稳定的创建方法,而这两个创建方法又是一般程序语言所自然提供的。如果要绕开这种创建带来的紧耦合,一个有效的创建模式就是工厂方法。
工厂方法的基本原理是用一个创建方法来返回一个创建对象,它定义了一个用于创建对象的接口,让子类决定实例化哪一个类,使得一个类的实例化延迟到子类。
以下面一个文件分割器的例子来讲解,我们对于不同的文件类型需要有不同的文件分割器:
从策略模式我们知道,对于这种情况我们首先要用抽象类的多态实现来代替switch-case,ISplitter接口与其下BinarySplitter,TxtSplitter,PictureSplitter,VideoSplitter就用来实现策略模式。
但是这会带来一个问题,我们在使用的时候如何告诉代码我们用的是哪种策略,如果在MainForm直接创建一个特定的new对象,这必然导致了代码的紧耦合。为了解决这个问题,我们首先定义了工厂基类SplitterFactory,其下定义了创建对象的虚函数CreateSplitter,而后用具体的工厂BinarySplitterFactory,TxtSplitterFactory,PictureSplitterFactory,VideoSplitterFactory来实现具体的创建(返回一个对应的创建对象return new),这本质上也是对SplitterFactory的策略化。经过这样子处理后,我们在MainForm里创建Splitter对象时就可以使用factory->CreateSplitter()的方法了,而factory具体是什么工厂则由外部传入,体现了变化被压缩到局部并延迟实现的思路,MainForm内部不再存在依赖于具体类的new方法。
ISplitter* Splitter=factory->CreateSplitter()和Splitter->split()均为不依赖细节的多态调用。
工厂方法的局限性在于不同工厂的创建方法(参数表,返回形式)必须相同。
7.抽象工厂(Abstract Factory):
用于应对一系列相互依赖的对象的创建工作,如果各对象之间不存在相互依赖的情况,则抽象工厂与普通的工厂方法无异,换句话说,工厂方法是抽象工厂在对象互不依赖时的特例。
以下面这个数据库操作的模型为例:
我们访问一个数据库有很多方法,这里列举了三种Connection,Command和DataReader,
在使用这些方法时是通过创建相应的对象后调用其下的成员函数实现的。
同时,我们针对不同的数据库时,上述的三种方法又有些许差距,例如SqlServer和Oracle
同理,我们首先根据策略模式对Connection,Command和DataReader进行接口的多态化
此时若按照一般的工厂方法,接下来就是对具体是创建SqlServer还是Oracle来定义工厂,
而不论是针对SqlServer还是Oracle,我们都需要创建三个对象Connection,Command和DataReader来调用他们的成员方法,相比于工厂方法中只需要创建一个分割器splitter复杂不少,此时我们如果将IDBConnection,IDBCommand和IDBDataReader各自定义一个对应的工厂IDBConnectionFactory,IDBCommandFactory和IDBDataReaderFactory会带来一个问题,由于Connection,Command和DataReader操作在同一时间调用时必然需要在同一数据库环境下,故此时会带来传入的不稳定性,因此我们应该把IDBConnectionFactory,IDBCommandFactory和IDBDataReaderFactory统一在一个工厂中IDBFactory,这样做我们对IDBFactory只能同时传入SqlDBFactory和OracleDBFactory之一,保证了对象之间的相互依赖关系。
//这里有误,SqlDBFactory应当是对IDBFactory各虚函数的实例化过程
//Oracle部分略去
//这里的dbFactory具体取何值仍由外部传入,构造函数略