设计模式-23种(二)
组件协同模式
现代软件专业分工之后的第一个结果是「框架与应用程序的划分」,「组件协作」模式通过晚期绑定,来实现框架与应用程序之间的松耦合,是二者之间协作时的常用模式。
下面是组件协同模式的三种典型模式。
Template Method(模板方法)
模板方法,就像高中老师讲的做题步骤一样,比如高考卷里的圆锥曲线题。
- 先设直线方程
- 把直线方程代入圆锥曲线
- 利用求根公式的定理,算出
- …
很多时候,题目都可以用这样一个通用的模板方法来解决,只需换掉一丢丢的不同,大致步骤都是一样。
在代码编程中,模板方法就使用的相当多,通常,子类只需要重写父类给出的「可重写的方法」即可。
//base class
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; //变化
};
这个例子中base类给出了两个纯虚函数,我们可以override这两个函数,做一些符合自己的变化。
在调用的时候只需如下即可。
int main(){
Library * app = new Application(); //Application 是Library的子类,并且重写了两个纯虚虚函数
app->run();
}熟悉安卓开发的同学,应该非常熟悉,很多时候,都是重写一个函数,例如生命周期的resume,stop函数等等,系统会自动调用,你重写之后的方法,是不是很神奇嘞,其中的奥秘就是这样捏。
总结
定义:「定义一个操作中的算法的骨架(稳定),而将一些步骤延迟(变化)到子类中。Template Method使得子类可以不改变(复用)一个算法的结构即可重定义(override重写)该算法的某些特定步骤」
下面是它的结构图。
模板方法是不是很好用,不要你调用我!让我来调用你。
Strategy(策略模式)
策略模式特别像模板方法。举个栗子,就能马上明白。
比如应用程序切换语言。
语言可能以后还会添加,可能有些人会这样写程序。
enum Language{
ZH_CN,
ENGLISH
};
int main(){
Language cur_set_lang = get_cur_lang();
if(cur_set_lang==ZH_CN){
...//中文处理
}else if(cur_set_lang==ENGLISH){
...//英文处理
}
}试想一下,我们现在要增加另一门语言,比如台湾繁体。在上述代码里,就要在枚举类型中添加,同时还要在main函数中的if判断中增加新的处理。
当项目大起来时,直接修改,往往会令人抓狂。这十分的不优雅。
策略模式,就可以很好的解决这个问题。
可以看出来,每个if中都要对相应的语言做处理,我们可以给它抽象出来。
class Language{
public:
virtual void process()=0;
virtual ~Language(){}
};天啊,看到这里,相比你已经知道了后续部分了,没错就像模板方法一样。
//中文类cpp文件
class Chinese:public Language{
virtual void process()override{
...//中文处理
}
virtual ~Chinese(){
...//相关资源释放
}
};
//英文类cpp文件
class English:public Language{
virtual void process()override{
...//英文处理
}
virtual ~English(){
...//相关资源释放
}
};
如此这般,添加新语言,只需要创建一个新的class类文件即可,避免了直接在源码上修改。
调用过程也变的简洁明了。
//值得一提的是,一般会与工厂方法一起使用,因为工厂方法还没介绍,先不要啦。
int main(){
Language* cur_set_lang = get_cur_lang();
cur_set_lang->process();
}
总结
定义:「定义一系列算法,把它们一个个封装起来,并且使他们可互相替换(变化)。该模式使得算法可独立于使用它的客户程序(稳定)而变化(扩展,子变化)。」
结构如下
策略模式,可以很好的解决if else 这种没有止境或者不确定后续是否还会添加其他选项的代码,实在是妙啊。
Observe | Event(观察者模式)
观察者一般都是站在第三方角度上,observe中文有着「观察」的意思,而Event有着「事件」的意思,从字面意思上直观了解,不是那么容易懂。举个例子,当一个程序跑起来了,我们想看一下中间过程中发生的情况,这时我们就可以插入一段「代码(像Log)」可以让我们观察程序中间过程中的执行情况,也可以把这个观察,说成一种事件,「观察程序中间过程」事件。
比如一个分割文件的程序,我们想加一个进度条来显示实时进度,先来看一个简单的例子。
//main.cpp
class MainForm : public Form
{
TextBox* txtFilePath;
TextBox* txtFileNumber;
ProgressBar* progressBar;
public:
void Button1_Click(){
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());
FileSplitter splitter(filePath, number, progressBar);
splitter.split();
}
};在上面这段代码里,我们像FileSplitter类中传入了一个进度条实例来显示进度。
如果我们的FileSplitter类如下。
//FileSplitter.cpp
class FileSplitter
{
string m_filePath;
int m_fileNumber;
ProgressBar* m_progressBar;
public:
FileSplitter(const string& filePath, int fileNumber, ProgressBar* progressBar) :
m_filePath(filePath),
m_fileNumber(fileNumber),
m_progressBar(progressBar){
}
void split(){
//1.读取大文件
//2.分批次向小文件中写入
for (int i = 0; i < m_fileNumber; i++){
//...
float progressValue = m_fileNumber;
progressValue = (i + 1) / progressValue;
m_progressBar->setValue(progressValue);
}
}
};
这样写没毛病,让我们试想一下,如果此时我们再加一个进度条,比如在终端上打印进度信息,或者再加几个观察进度程序,如此上面的FileSplitter类将不能满足我们的要求。
『我们有时需要让一个类引起的变化,通知到其他一个或多个类。 』
继续之前的例子,按照我们的要求,当FileSplitter类变化时,其中我们加入的多个进度条类也会发生变化,也就是说FileSplitter类变化会通知其他进度条类。
这些需要FileSplitter类应该具有一些共同的函数等,可以把这些东西给抽象为一个接口。
例如这样
class IProgress{
public:
virtual void DoProgress(float value)=0;
virtual ~IProgress(){}
};此时FileSplitter类就要这样修改了
class FileSplitter
{
string m_filePath;
int m_fileNumber;
List<IProgress*> 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){
auto itor=m_iprogressList.begin();
while (itor != m_iprogressList.end() )
(*itor)->DoProgress(value); //更新进度条
itor++;
}
}
};我们用一个vector容器存储需要通知的类,这些需要通知的类,需要继承IProgress接口,并且实现其中的纯虚函数,如此,我们就可以这样很方便的完成需求。
调用如下
class MainForm : public Form, public IProgress
{
TextBox* txtFilePath;
TextBox* txtFileNumber;
ProgressBar* progressBar;
public:
void Button1_Click(){
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());
ConsoleNotifier cn;
FileSplitter splitter(filePath, number);
splitter.addIProgress(this); //订阅通知
splitter.addIProgress(&cn); //订阅通知
splitter.split();
splitter.removeIProgress(this);
}
virtual void DoProgress(float value)override{
progressBar->setValue(value);
}
};
class ConsoleNotifier : public IProgress {
public:
virtual void DoProgress(float value)override{
cout << ".";
}
};总结
定义:「定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。」
结构如下
是不是有种学到啦的感觉,观察者模式,可以让一个类通知许多类的改变,仅仅把这些的共同地方抽象出来,就可以让 代码变的健壮起来。
单一职责模式
在软件组件的设计中,如果责任划分的不清晰,使用继承得到的结果往往是随着需求的变化,子类急剧膨胀,同时充斥着重复代码,这时候的关键是划清责任。
下面介绍两种经典的单一职责模式可以很快帮助我们理解。
Decorator(装饰器模式)
比如我们要做一个基于TCP的文件传输的小工具,IO方面,我们肯定需要处理文件流、网络流,甚至可能需要对这些流进行加密,或者对流进行缓存。
那么如果我们这些写,先抽象一个Stream的抽象类,包含read(),seek(),write() 三种方法。
class Stream{
public:
virtual char Read(int number)=0;
virtual void Seek(int position)=0;
virtual void Write(char data)=0;
virtual ~Stream(){}
};很容易就会想到文件流、网络流继承Stream。
class FileStream{
public:
virtual char Read(int number){
...
}
virtual void Seek(int position){
...
}
virtual void Write(char data){
...
}
virtual ~Stream(){}
};
class NetworkStream{
public:
virtual char Read(int number){
...
}
virtual void Seek(int position){
...
}
virtual void Write(char data){
...
}
virtual ~Stream(){}
};假设现在我们需要加密流和缓存流,新手一般会这样写。
class CryptoFileStream :public FileStream{
public:
virtual char Read(int number){
//额外的加密操作...
FileStream::Read(number);//读文件流
}
virtual void Seek(int position){
//额外的加密操作...
FileStream::Seek(position);//定位文件流
//额外的加密操作...
}
virtual void Write(byte data){
//额外的加密操作...
FileStream::Write(data);//写文件流
//额外的加密操作...
}
};
class CryptoNetworkStream : :public NetworkStream{
public:
virtual char Read(int number){
//额外的加密操作...
NetworkStream::Read(number);//读网络流
}
virtual void Seek(int position){
//额外的加密操作...
NetworkStream::Seek(position);//定位网络流
//额外的加密操作...
}
virtual void Write(byte data){
//额外的加密操作...
NetworkStream::Write(data);//写网络流
//额外的加密操作...
}
};
//-------------------
//缓存流
class BufferFileStream:public FileStream{
...
};
class BufferNetworkStream:public FileStream{
...
};现在,回头看一下代码,很明显,每当我们去添加一个扩展功能类,比如反转流,这样就要再继承 FileStream、NetworkStream写两个类,或者当我们去添加一个流,比如内存流,这样就要继承内存流去写很多扩展类。
假设,FileStream、NetworkStream这样的类有N个,加密流这样的扩展有M个,那么一共要写个。
不难发现,在扩展类中,实现的东西都相同,很容易联系到多态。那么扩展类就可以写成下面这样,比如CryptoStream类。
class CryptoStream :public Stream{
Stream* stream;
public:
CryptoStream(Stream* s):stream(s){
}
virtual char Read(int number){
//额外的加密操作...
stream->Read(number);//读文件流
}
virtual void Seek(int position){
//额外的加密操作...
stream->Seek(position);//定位文件流
//额外的加密操作...
}
virtual void Write(byte data){
//额外的加密操作...
stream->Write(data);//写文件流
//额外的加密操作...
}
};如此我们就可以省下很多重复的代码,只需要,非常nice。但是当我写出BufferStream类时,你会发现这其中又可以提出来共同的成员。
class BufferStream :public Stream{
Stream* stream;
public:
BufferStream(Stream* s):stream(s){
}
...
};可以看到stream这个成员变量重复了,可以把这个给抽出来,单独写成一个类。
DecoratorStream: public Stream{
protected:
Stream* stream;//...
DecoratorStream(Stream * stm):stream(stm){
}
};这样在以后写扩展类时,继承这个DecoratorStream,既可以清楚意图,又减少了代码的重复。
例如BufferStream类就可以写成下面这样。
class BufferStream : public DecoratorStream{
public:
BufferStream(Stream* stm):DecoratorStream(stm){
}
//...
};调用程序如下
void Process(){
//运行时装配
FileStream* s1=new FileStream();
CryptoStream* s2=new CryptoStream(s1);
BufferStream* s3=new BufferedStream(s1);
BufferStream* s4=new BufferedStream(s2);
}总结
定义:动态(组合)地给一个对象增加一些额外的职责。就增加功能而言,Decorator模式比生成子类(继承)更为灵活(消除重复代码&减少子类个数)
结构如下
仔细想想,其实CryptoFileStream继承FileStream是不合理的,因为前者仅仅是后者的一个扩展,并没有is a的关系,也不朝着同一个方向变化。
Bridge(桥模式)
桥模式与装饰器模式十分相似,都是为了解决继承使得类数量爆炸增长的问题。
举个例子,现在要开发一个即时通讯软件,新手一般会先抽象一个类。
比如
class Messager{
public:
virtual void Login(string username, string password)=0;
virtual void SendMessage(string message)=0;
virtual void SendPicture(Image image)=0;
virtual void PlaySound()=0;
virtual void DrawShape()=0;
virtual void WriteText()=0;
virtual void Connect()=0;
virtual ~Messager(){}
};这很不错,PC和mobile端继承Messager,后续再继承pc类开发pc上的不同版本,例如lite,prefect版本等等。
class PCMessagerBase : public Messager{
public:
virtual void PlaySound(){
//**********
}
virtual void DrawShape(){
//*********
}
virtual void WriteText(){
//**********
}
virtual void Connect(){
//**********
}
};
class MobileMessagerBase : public Messager{
public:
...
};接着写一下PC端的版本。
class PCMessagerLite : public PCMessagerBase {
public:
virtual void Login(string username, string password){
PCMessagerBase::Connect();
//........
}
virtual void SendMessage(string message){
PCMessagerBase::WriteText();
//........
}
virtual void SendPicture(Image image){
PCMessagerBase::DrawShape();
//........
}
};
class PCMessagerPerfect : public PCMessagerBase {
public:
virtual void Login(string username, string password){
PCMessagerBase::PlaySound();
//********
PCMessagerBase::Connect();
//........
}
virtual void SendMessage(string message){
PCMessagerBase::PlaySound();
//********
PCMessagerBase::WriteText();
//........
}
virtual void SendPicture(Image image){
PCMessagerBase::PlaySound();
//********
PCMessagerBase::DrawShape();
//........
}
};这仅仅是PC端的,如果算上mobile端,又要加上两个业务类,加一个Linux平台,可能就要一个Linux base类,和两个业务类。
假如有N个平台,M个版本 ,那么总共就要写个类,想想就十分可怕。
学习过上一节,很自然就会想「能不能使用多态来代替这个继承」。
首先我们先把Messager类分开,因为pc base类与mobile base类,仅仅实现了基础的部分,并没有实现login这些功能。
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(){}
};
class MessagerImp{
public:
virtual void PlaySound()=0;
virtual void DrawShape()=0;
virtual void WriteText()=0;
virtual void Connect()=0;
virtual MessagerImp(){}
};下面只需让pc base类与mobile base类去实现MessagerImp接口,而业务类继承Messager类。
class PCMessagerImp : public MessagerImp{
public:
virtual void PlaySound(){
//**********
}
virtual void DrawShape(){
//**********
}
virtual void WriteText(){
//**********
}
virtual void Connect(){
//**********
}
};
class MobileMessagerImp : public MessagerImp{
public:
...
};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();
//........
}
};这样类的数量就缩短到了.
调用主程序,可以写成如下格式。
PCMessagerImp * pc = new PCMessagerImp();
Messager* mp = new MessagerPerfect(pc);
Messager* ml = new MessagerLite(pc);
总结
定义:将抽象部分(业务功能)与实现部分(平台实现)分离,使它们都可以独立地变化。
结构如下
桥模式与装饰器模式,都有异曲同工之妙,都是将继承换为多态,总而避免了类数量的急剧增长。
对象创建模式
通过“对象创建” 模式绕开new,来避免对象创建(new)过程中所导致的紧耦合(依赖具体类),从而支持对象创建的稳定。它是接口抽象之后的第一步工作。
简单来说,就是让这个类文件中new的对象,像virtual函数那样,晚绑定。
也就是依赖倒置原则,不依赖与实现,应该依赖与抽象。
如果一个类文件中,有一处实现,那么可能在以后的过程中,就要去改这个类文件。