设计模式
简述
- 如何解决复杂性
- 分解:分而治之,由大化小
- 抽象:忽略它的非本质细节,去处理泛化和理想化了的对象模型
- 8大原则
- 依赖倒置原则(DIP)
- 高层模块不应该依赖于低层模块,二者都应该依赖于抽象
- 抽象不应该依赖于实现细节,实现细节应该依赖于抽象
- 开放封闭原则(OCP)
- 对扩展开放,对更改封闭
- 类模块应该是可扩展的,但是不可修改
- 单一职责原则(SRP)
- 一个类应该仅有一个引起它变化的原因
- 变化的方向隐含着类的责任
- Liskov 替换原则(LSP)
- 子类必须能够替代它们的基类(IS-A)
- 继承表达类型抽象
- 接口隔离原则(ISP)
- 不应该强迫客户程序依赖它们不用的方法
- 接口应该小而完备
- 优先使用对象组合,而不是类继承
- 类继承通常为“白箱复用”,对象组合通常为“黑箱复用”
- 继承在某种程度上破坏了封装性,子类父类耦合度高
- 而对象组合则要求被组合的对象具有良好定义的接口,耦合度低
- 封装变化点
- 使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合
- 针对接口编程,而不是针对实现编程
- 不将变量类型声明为某个特定的具体类,而是声明为某个接口
- 客户程序无需获知对象的具体类型,只需知道对象所具有的的接口
- 减少系统中各部分的依赖关系,从而实现“高内聚,松耦合”的类型设计方案
- 依赖倒置原则(DIP)
- 重构关键技法
- 静态 -> 动态
- 早绑定 -> 晚绑定
- 继承 -> 组合
- 编译时依赖 -> 继承时依赖
- 紧耦合 -> 松耦合
一、组件协作
通过晚期绑定,实现框架与应用程序之间的松耦合
1.Template Method
-
在框架结构稳定的情况下,利用虚函数的多态性,实现晚绑定,“不要调用我,让我来调用你”,使得子类在不改变一个算法的结构即可重定义该算法的某些特定步骤
// 整体流程固定,其中稳定的方法写为非虚的,变化的部分定义为虚函数,使用时由子类继承基类,并重写其中定义的虚函数 class Library { public: //稳定 template method void Run() { Step1(); if (Step2()) { //支持变化 ==> 虚函数的多态调用 Step3(); } for (int i = 0; i < 4; i++) { Step4(); //支持变化 ==> 虚函数的多态调用 } Step5(); } virtual ~Library() {} protected: void Step1() { //稳定 //..... } void Step3() { //稳定 //..... } void Step5() { //稳定 //..... } virtual bool Step2() = 0; //变化 virtual void Step4() = 0; //变化 };
2.Strategy 策略模式
定义一系列的算法,把它们封装起来,并且使它们可互相替换。在运行时根据需要透明的更改对象的算法
-
解耦合:将条件判断语句转换为Strategy模式
// 通过工厂方法创建对应的对象,并利用虚函数机制调用其实现的算法 class TaxStrategy { public: virtual double Calculate(const Context& context) = 0; virtual ~TaxStrategy() {} }; class SalesOrder { private: TaxStrategy* strategy; public: SalesOrder(StrategyFactory* strategyFactory) { this->strategy = strategyFactory->NewStrategy(); } ~SalesOrder() { delete this->strategy; } public double CalculateTax() { //... Context context() double val = strategy->Calculate(context); //... } };
3.Observer 观察者模式
- 建立一种“通知依赖关系”,一个对象的状态发生改变,所有的依赖对象都将得到通知。
-
目标发送通知时,无需指定观察者,通知会自动传播。订阅者自己决定是否需要订阅,目标对象对此一无所知。
// 订阅者抽象接口 class IProgress { public: virtual void DoProgress(float value) = 0; virtual ~IProgress() {} }; class FileSplitter { string m_filePath; int m_fileNumber; List
m_iprogressList; // 抽象通知机制,支持多个观察者 public: FileSplitter(const string& filePath, int fileNumber) : m_filePath(filePath), m_fileNumber(fileNumber) {} void split() { // 1.读取大文件 // 2.分批次向小文件中写入 for (int i = 0; i < m_fileNumber; i++) { //... float progressValue = m_fileNumber; progressValue = (i + 1) / progressValue; onProgress(progressValue); //发送通知 } } void addIProgress(IProgress* iprogress) { m_iprogressList.push_back(iprogress); } void removeIProgress(IProgress* iprogress) { m_iprogressList.remove(iprogress); } protected: virtual void onProgress(float value) { List ::iterator itor = m_iprogressList.begin(); while (itor != m_iprogressList.end()) { (*itor)->DoProgress(value); //更新进度条 itor++; } };
二、单一职责
责任划分的不清晰,会导致子类急剧膨胀,充斥着重复代码。这时关键是划清责任。
4.Decorator 装饰模式
- 继承会为类型引入静态特质,使得扩展方式缺乏灵活性,导致“过度地使用继承来扩展对象的功能”,使得子类急剧膨胀
动态(组合)地给一个对象增加一些额外的职责。就增加功能而言,Decorator模式比生成子类(继承)更为灵活(消除重复代码&减少子类个数)。 ———— 《设计模式》 GOF
- Decorator类在接口上表现为is-a Component的继承关系,即Decorator类继承了Component类所具有的接口。但再实现上又表现为has-a Component的组合关系,即Decorator类又使用了另外一个Component类
//业务操作 class Stream { public: virtual char Read(int number) = 0; virtual void Seek(int position) = 0; virtual void Write(char data) = 0; virtual ~Stream() {} }; //主体类 class FileStream : public Stream { public: virtual char Read(int number) { //读文件流 } virtual void Seek(int position) { //定位文件流 } virtual void Write(char data) { //写文件流 } }; class NetworkStream : public Stream { public: virtual char Read(int number) { //读网络流 } virtual void Seek(int position) { //定位网络流 } virtual void Write(char data) { //写网络流 } }; class MemoryStream : public Stream { public: virtual char Read(int number) { //读内存流 } virtual void Seek(int position) { //定位内存流 } virtual void Write(char data) { //写内存流 } }; //扩展操作 DecoratorStream : public Stream { protected: Stream* stream; //... DecoratorStream(Stream * stm) : stream(stm) {} }; class CryptoStream : public DecoratorStream { public: CryptoStream(Stream* stm) : DecoratorStream(stm) {} virtual char Read(int number) { //额外的加密操作... stream->Read(number); //读文件流 } virtual void Seek(int position) { //额外的加密操作... stream->Seek(position); //定位文件流 //额外的加密操作... } virtual void Write(byte data) { //额外的加密操作... stream->Write(data); //写文件流 //额外的加密操作... } }; class BufferedStream : public DecoratorStream { Stream* stream; //... public: BufferedStream(Stream* stm) : DecoratorStream(stm) {} //... }; void Process() { //运行时装配 FileStream* s1 = new FileStream(); CryptoStream* s2 = new CryptoStream(s1); BufferedStream* s3 = new BufferedStream(s1); // 加密&缓存 BufferedStream* s4 = new BufferedStream(s2); }
5.Bridge 桥模式
- 某些类型的固有的实现逻辑,使得他们具有两个(以上)变化的维度
将抽象的部分(业务功能)与实现的部分(平台实现)分离,使它们都可以独立地变化。 ————《设计模式》 GOF
- 将类中抽象和实现的部分抽离开来,分别“子类化”它们。在抽象类中包含一个指向实现类的指针,客户端只需实现抽象类定义的接口,无需关心具体的实现。
class MessagerImp { public: virtual void PlaySound() = 0; virtual void DrawShape() = 0; virtual void WriteText() = 0; virtual void Connect() = 0; virtual ~MessagerImp() {} }; class Messager { protected: MessagerImp* messagerImp; //... public: virtual void Login(string username, string password) = 0; virtual void SendMessage(string message) = 0; virtual void SendPicture(Image image) = 0; virtual ~Messager() {} }; //平台实现 n class PCMessagerImp : public MessagerImp { public: virtual void PlaySound() { //********** } virtual void DrawShape() { //********** } virtual void WriteText() { //********** } virtual void Connect() { //********** } }; class MobileMessagerImp : public MessagerImp { public: virtual void PlaySound() { //========== } virtual void DrawShape() { //========== } virtual void WriteText() { //========== } virtual void Connect() { //========== } }; //业务抽象 m //类的数目:1+n+m class MessagerLite : public Messager { public: virtual void Login(string username, string password) { messagerImp->Connect(); //........ } virtual void SendMessage(string message) { messagerImp->WriteText(); //........ } virtual void SendPicture(Image image) { messagerImp->DrawShape(); //........ } }; class MessagerPerfect : public Messager { public: virtual void Login(string username, string password) { messagerImp->PlaySound(); //******** messagerImp->Connect(); //........ } virtual void SendMessage(string message) { messagerImp->PlaySound(); //******** messagerImp->WriteText(); //........ } virtual void SendPicture(Image image) { messagerImp->PlaySound(); //******** messagerImp->DrawShape(); //........ } }; void Process() { //运行时装配 MessagerImp* mImp = new PCMessagerImp(); Messager* m = new Messager(mImp); }
三、对象创建
通过“对象创建”模式绕开new,来避免对象创建(new)过程中所导致的紧耦合(依赖具体类),从而支持对象创建的稳定。是接口抽象之后的第一步工作。
6.Factory Method 工厂方法
- 由于需求的变化,需要创建的对象的具体类型经常变化
定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method使得一个类的实例化延迟(目的:解耦,手段:虚函数)到子类。 ———— 《设计模式》 GOF
工厂方法能够隐藏的类名,避免构造参数,避免在构造方法改变的时候能够避免修改每个new
-
工厂方法有什么用?
ISplitterFactor.h //抽象类 class ISplitter { public: virtual void split() = 0; virtual ~ISplitter() {} }; //工厂基类 class SplitterFactory { public: virtual ISplitter* CreateSplitter() = 0; virtual ~SplitterFactory() {} }; FileSplitter.h //具体类 class BinarySplitter : public ISplitter {}; class TxtSplitter : public ISplitter {}; class PictureSplitter : public ISplitter {}; class VideoSplitter : public ISplitter {}; //具体工厂 class BinarySplitterFactory : public SplitterFactory { public: virtual ISplitter* CreateSplitter() { return new BinarySplitter(); } }; class TxtSplitterFactory : public SplitterFactory { public: virtual ISplitter* CreateSplitter() { return new TxtSplitter(); } }; class PictureSplitterFactory : public SplitterFactory { public: virtual ISplitter* CreateSplitter() { return new PictureSplitter(); } }; class VideoSplitterFactory : public SplitterFactory { public: virtual ISplitter* CreateSplitter() { return new VideoSplitter(); } }; MainForm.cpp class MainForm : public Form { SplitterFactory* factory; //工厂 public: MainForm(SplitterFactory* factory) { this->factory = factory; } void Button1_Click() { ISplitter* splitter = factory->CreateSplitter(); //多态new splitter->split(); } };
作业
在做这周设计模式的作业时对实现“FileSplitter支持多种文件分割算法”应该模板方法模式还是策略模式纠结一下,通过对查阅相关资料得到了解答,遂记录于此。
作业题目[1]:
考虑一个文件分割器的设计。MainForm为界面类,收集用户输入的文件路径和分割数量。FileSpliter为实现文件分割的类型,其中Split()实现文件分割算法。1.要求为Split()支持多种文件分割算法(至少三种),在MainForm中灵活切换多种算法。2.在Split()分割过程中,实现对进度条的实时通知,即对ProgressBar赋值。3.使用松耦合面向对象设计方法和思想,无需编写具体算法实现,可使用伪码表示设计。
在实现Split的过程,发现模板方法和策略模式都可以满足题目的要求。模板方法以FileSplitter为基类,在Split实现文件分割的大流程和对进度条的实时通知,将真正实现切割的代码doSplit声明为保护方法,让子类覆盖。子类通过覆盖父类的doSplit来实现算法定制。为达到在MainForm自由切换算法的目的,可以使用简单工厂模式来自由生产FileSplitter实例。类结构图如下:
同样该问题也可以使用策略模式来解决。策略模式首先声明一个ISplitStragy接口,该方法包含了一个实现算法分割的接口doSplit。各个文件分割策略都实现ISplitStragy接口,然后FileSplitter包含一个ISplitStragy对象并在Split()函数中调用该策略。同样,为了实现在MainForm中自由的切换算法,可以使用简单工厂模式来自由生产FileSplitter实例。类结构图如下:
通过上述的讲解,我们发现该题完全可以通过模板方法/策略模式来实现,而且也看不出谁优谁劣。那么这两种方法是否等同了吗?显然不是。
首先,我们来看下这两种模式的意图。
策略模式[2]
定义一系列的算法,把它们一个个封装起来, 使它们可相互替换。本模式使得算法可独
立于使用它的客户而变化。
模板方法[2]
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。TemplateMethod使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
从意图上来看,两者都在试图解决算法多样性对代码结构的冲击。只是,策略模式通过将算法封装成类,通过组合使用这些类。而模式方法则将算法的可变部分封装成Hook,由子类定制。
从定义上来看,模式方法更加侧重于业务流程相对复杂且稳定,而其中的某些步骤(局部变化)变化相对剧烈的场景。而策略模式则是偏重于算法本身(整个算法)就变化相对距离的情形。因此,当使用场景中业务流程相对简单且稳定的情况,使用策略模式和模板方法都是可以得,但是更推荐用模板方法(模板方法更灵活)。
综上:模板方法和策略模式都是解决算法多样性对代码结构冲击的问题。模板方法使用与业务场景相对复杂且稳定的情况,策略模式使用与算法相对多样灵活的场景。当业务相对简单时,策略模式和模板方法几乎等效,但是推荐使用策略模式。