前言
设计模式一直是程序员津津乐道的事情,经常codereview的时候就会有人提出,这个代码不符合XX设计原则或者XX设计模式。关于设计模式的书籍市场上也是林林种种,多如牛毛。笔者有幸拜读了GOF(gang of four)的神作《设计模式--可复用的面向对象软件的基础》在感慨四位大师智慧的同时不得不承认有些模式确实是已经跟不上时代了,毕竟这本书是1995年出版的,限于当时机器的一些硬件(内存,cpu等)原因,还有当时一些高级的语言和数据结构和标准没有形成,所以书中会描述一些在今天看来已经跟不上潮流的模式。本文不打算对GOF的23种设计模式一一详细描述,有些比较有共鸣的模式会有具体的代码示例和详细描述,一些没有共鸣的模式可能就一笔带过了,本文中所有的示例都是C++的伪代码,或者是一部分代码。C++实现设计模式就要强依赖虚函数,虚函数可以在运行时动态绑定具体的函数,从而给了程序更多的可拓展性。
设计原则
要看一段代码是不是好的代码不是要生搬硬套看它符合什么样的模式,而是看它是否符合下面的六个设计原则。设计模式只是一个表面的套路,设计原则才是中心思想。或者说设计模式只是一个手段,设计原则才是目的。正如张三丰对张无忌对话的一样:
“无忌你记住了吗”
“全忘了”
“那你可以上场了”
设计模式会随着语言的进步逐步变化和演进,但是设计原则是不会变的,所以我们要把握中根本的东西。以下是六大设计原则:
1.依赖倒置原则:高层模块不应该依赖底层模块。
2.开闭原则:对拓展开放,对修改封闭。
3.单一职责原则:一个类的职责只有一个。
4.替换原则(里氏替换原则):子类必须能够替换父类。
5.接口隔离原则:暴露给用户的接口小而完备。
6.迪米特法则:一个对象应该对其他对象保持最少了解。
23种设计模式
GOF把23种设计模式分为三类,分别属于创建型模式,结构型模式,行为模式。
创建型模式
1.抽象工厂(abstract factory)模式
2.生成器(builder)模式
3.工厂方法(factory method)模式
4.原型(prototype)模式
5.单例(singleton)模式
工厂模式说白了就是return一个new出来的对象,抽象工厂就是new回来一堆相关的对象,生成器模式就是一个负责对象的分步创建并且将一些步骤定义成虚函数,而外部继承实现。原型模式就是通过拷贝自身来创建对象。单例模式更是被大家用烂的模式,一个类只有一个实例。这些都是用一句话就能说清楚的模式,后边不会再赘述。
结构型模式
6.适配器(adapter)模式
7.桥接(bridge)模式
8.组合(composite)模式
9.装饰模式(decorator)模式
10.外观(facade)模式
11.享元(flyweight)模式
12.代理(proxy)模式
行为模式
13.职责链(chain of responsibility)模式
14.命令(Command)模式
15.解释器(interpreter)模式
16.迭代器(iterator)模式
17.中介者(mediator)模式
18.备忘录(memento)模式
19.观察者(observer)模式
20.状态(state)模式
21.策略(strategy)模式
22.模板方法(template method)模式
23.访问者(visitor)模式
有共鸣的模式
这一节中会结合在项目中的代码给出示例代码,有一些是当前项目已经在用的模式,有一些是未来代码重构会用到。
策略模式(Strategy)
适配器(adapter)
模板方法(template method)
装饰器(decerator)
观察者(observer)
策略模式(Strategy)
GOF定义
定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。
类图(摘自GOF)结构
策略模式的应用几乎是无处不在的,只要有if/else的地方其实都可以用策略模式。所以在我们的项目中也是随处可以,这里我先贴一个没有使用策略模式的代码,对比一下理解可能会更深刻。
1 void messageHandle() 2 { 3 ...... 4 5 QueueItem* data = NULL; 6 worker->m_dataQ.pop(data); 7 8 if (NULL == data) 9 { 10 // record error and return
11 return; 12 } 13 14 if (MSG_FILE_COPY_BT_SYNC == data->m_itemtype ) 15 { 16 Worker->handleBTSync(data); 17 } 18 else if( MSG_FILE_COPY_BT_SVR_REFLUSH == data->m_itemtype) 19 { 20 worker->handleUploadData(data); 21 } 22 else if (MSG_FILE_COPY_BT_META == data->m_itemtype) 23 { 24 worker->handleDatameta(data); 25 } 26 else if(MSG_FILE_COPY_BT_PIECE_TCP == data->m_itemtype) 27 { 28 worker->handleClientPieceRequest(data); 29 } 30 else 31 { 32 ......34 } 35 ....... 36 }
这段没有使用策略模式貌似也没有什么问题,但是万一又增加一个消息类型呢?这个时候我们想到的一个最简单粗暴的方法就是在这段源代码上再加一个else if,但是这就违反了设计模式的开闭原则了。因为修改了原来的代码就意味着引进了新的风险,测试的时候就要测试这段代码,也就是说我们没有复用这段代码。
策略模式示例代码:
1 void messageHandle() 2 { 3 ...... 4 QueueItem* data = NULL; 5 worker->m_dataQ.pop(data); 6 7 if (NULL == data) 8 { 9 // record error and return
10 return; 11 } 12 13 Data->handleItem(); 14 ........ 15 }
Data都是QueueItem的子类实例化对象,这些子类都各自实现了handleItem()方法,所以调用方无需关心是什么消息类型,直接从无锁队列中取出来统一调用handleItem()方法即可。如果未来又增加一个消息类型,只需要再继承QueueItem实现一个子类即可,这部分的调用代码是一点都不用改的,从而实现真正的复用。
适配器(adapter)
GOF定义:
将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
类图(摘自GOF)结构
GOF介绍了两个方式来实现Adapter模式。第一种是使用多继承的方法,adapter类继承新类(Target)和旧类(Adaptee),然后在新接口中调用旧的接口,当然还有少量的兼容适配代码。第二种方法是继承新的类(Target)并且组合旧的类(Adaptee)。前者称为类适配器,后者称为对象适配器。我本人是比较推崇对象适配器的,因为多继承在一定的意义上是破坏了类的封装性,另外也不够灵活。据我所知,一些有经验的前辈都是推荐使用组合,而不是多继承。
项目中有一个apiserver对外提供服务,旧的接口已经用户在使用了,但是由于旧的接口定义比较窄(参数比较少以及场景考虑不到位),不能够满足新用户的需求(旧的用户是可以满足的),所以要定义一个全新的接口,但是旧的接口很多代码都是可以复用的,只要稍作调整和兼容,刚好满足adapter模式的使用场景,所以就拿来当做示例代码,使用的是对象适配器实现方式。
下面是示例代码:
1 class NewApi 2 { 3 public: 4 virtual int newApi1(); 5 }; 6 7 class OldApi 8 { 9 public: 10 int oldApi1(); 11 }; 12 13 class Adapter : pulblic NewApi 14 { 15 private: 16 OldApi* m_oldApi; 17 Public: 18 virtual int newApi1() 19 { 20 //少量兼容适配代码 21 m_oldApi->oldApi1(); 22 } 23 };
模板方法(template method)
GOF定义:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Template Method使得子类可以不改变一个算法的结构即可,重定义改算法的特定步骤。
类图(摘自GOF)结构
示例代码如下:
1 class Base(第三方开源库代码) 2 { 3 public: 4 void display() 5 { 6 doFocus(); 7 //some other code 8 doDisplay(); 9 //some other code 10 resetFocus(); 11 } 12 protected: 13 virtual void doDisplay() = 0; 14 Void doFocus(); 15 void resetFocus(); 16 }; 17 18 class subClass : public Base(应用代码) 19 { 20 Public: 21 Virtual void doDisplay() 22 { 23 //implement my doDisplay 24 } 25 }; 26 27 int main() (应用调用代码) 28 { 29 Base* base = new subClass(); 30 base->display(); 31 Return 0; 32 }
从以上例子中我们可以看出,Base定义了一个display的方法,display的算法框架是固定的,这里是先调用doFocus,在调用doDisplay(),再是resetFocus。但是其中的一步doDisplay是延迟到了子类的实现中,具体怎么doDisplay由子类决定。即“子类可以不改变一个算法的结构即可,重定义改算法的特定步骤”。然后main函数的调用是不用关心display的内部实现,它只需要调用基类的display方法即可。
这个设计模式常用的场景是当Base类是第三方开源库的某一个类,但是第三方库开发人员是无法知道特定的用户想怎么实现用户层的代码,所以把某个特定的步骤预留出来让用户自己实现是一种可行的办法。例如下面是libevhtp的用户示例代码:
其实这些步骤都是固定的(固定的算法骨架),不用用户在自己的代码里面再把这些示例的代码再复制粘贴过来,作者完全可以自己把它封装成一个函数,然后里面在调用特定的要用户自己设计的代码(特定步骤)再封装成另外一个虚函数让用户自己实现。这样用户的main函数里面只要有一行调用代码即可。
装饰器(decerator)
GOF定义
动态给对象添加一些额外的职责。就增加功能来说,Decorator模式相比生成子类更为灵活。
类图(摘自GOF)结构
有时候运用Decorator模式会比生成子类的方法少实现很多子类,即能减少子类的数目,从而节省开发的时间成本。
如上图所示,现在有两个Stream的子类,MemoryStream, FileStream,现在想实现一个ASCII转码功能和一个压缩功能,一般我们继承MemoryStream,然后将转码和压缩的功能实现一遍,然后另外子类继承FileStream,然后将转码和压缩的功能实现一遍。但是我们很容易发现尽管MemoryStream和FileStream的读写方法不同,但是它们转码和压缩的代码是一样的,那么久应该把转码和压缩分别作为子类,然后让MemoryStream和FileStream的对象作为参数动态的传递给转码和压缩的类对象,所以就有了上面的类图架构。
1 Class Stream 2 { 3 Public: 4 Virtual int read() =0; 5 Vritral int write() = 0; 6 }; 7 8 Class MemoryStream:public Stream 9 { 10 Public: 11 Int read() 12 { 13 //do memory stream read 14 } 15 Int wrtie() 16 { 17 //do memory stream write 18 } 19 }; 20 21 Class FileStream : public Stream 22 { 23 //同理 24 }; 25 26 Class Decorator:public Stream 27 { 28 Private: 29 Stream* m_stream; 30 31 Public: 32 Decorator(Stream* s) 33 { 34 m_stream = s; 35 } 36 37 Int Read() 38 { 39 m_stream->Read(); 40 } 41 }; 42 43 class CompressingStream : pulic Decorator 44 { 45 Public: 46 Int Read() 47 { 48 //compress data in buffer 49 Decorator::Read(); 50 } 51 };
这样实现我们就可以动态的给文件流增加转码和压缩的功能。例如组合一个转码压缩的文件流只需一行代码:
1 Stream* s = new CompressingStream(new ASCII7Stream(new FileStream()));
观察者(observer)
GOF定义
定义对象间的一种一对多的依赖关系,对一个对象的状态发生改变时,所有依赖它的对象都得到通知并被自动更新。
类图(摘自GOF)结构
观察者模式的核心其实就是观察者(observer)向目标(subject)注册一个回调,当目标观察到条件发生时就回调之前观察者注册的回调函数。
这个模式可以用到zookeeper的应用代码中,zookeeper在我们项目中扮演者服务发现和分布式配置和分布式锁的角色。当有很多个类对象都依赖于zookeeper的时候,可以让一个类(subject)专门watch zookeeper的状态,其他类(observer)向subject注册回调,当zookeeper发生状态改变的时候就回调这些回调函数。
1 Class ZKSubject 2 { 3 Private: 4 Vector<Base*> m_observerList; 5 Public: 6 Void attach(Base* base); 7 Void detach(Base* base); 8 Void notify() 9 { 10 For(int i = 0; i < m_observerLis.size(); ++i) 11 { 12 m_observerList[i]->update(this); 13 } 14 } 15 }; 16 17 Class Base 18 { 19 Public: 20 virtual void update(ZKSubject* s) = 0; 21 }; 22 23 Class SubClass1: public Base 24 { 25 Public: 26 Void update(ZKSubject* s) 27 { 28 //do youself about zookeeper 29 } 30 }; 31 32 Class SubClass2: public Base 33 { 34 Public: 35 Void update(ZKSubject* s) 36 { 37 //do youself about zookeeper 38 } 39 };
架构级模式
这两个模式讲究的是系统架构级的接口隔离。外观模式是让一堆紧耦合的类在系统的内部,然后外部只和一个接口类打交道,接口类负责内外的处理。这样暴露给外界的接口就小而完备,内部的改动也不会影响到外部应用。代理模式是本来应该调用A,但是由于这样或者那样的原因必须要经过B才能访问A,这样B就是A的代理类。典型的场景就是分布式使用thrift(一个异步RPC框架)接口的时候,我们如果直接调用对端的类,而是通过thrift的生成类去分布式(网络通信和序列化以及反序列化)调用。
外观模式(facade)
代理模式(proxy)
与数据结构相关的模式
以下的这四个设计模式是让调用方从一对多变成一对一,比如说职责链模式,调用方调用一个对象节点就相当于调用整个链表的节点,而是否调用下一个节点,怎么调用下一个节点调用方式不需要关心的,从而实现抽象调用。
职责链(chain of responsibility)链表
迭代器(Iterator)迭代器
组合(composite)树
享元模式(flyweight)map
职责链(chain of responsibility)链表
GOF定义
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
类图(摘自GOF)结构
职责链模式的核心就是链表,所有的对象组成一个链表,每个对象是其中的一个结点。当这个对象处理不了这个请求的时候就会将这个请求传递给它知道的下一个对象,如此递归,直到有一个对象处理它为止。
这个模式的应用只适合于请求者不知道哪一个接受者能处理它的情况,也就是说一定要遍历一遍的情况。假如说已经知道了请求者和接受者之间的对象关系,直接找到接收者处理它即可。例如我们项目的各种任务消息对象的dbproxy的写redis线程,这里就可以把任务消息看作是请求者,写redis的线程看作是接受者,假如消息和写redis线程的对象关系不知道,那就要一个个遍历这些redis线程,到底哪个线程能够处理这个消息(这里考虑到写redis的时序性问题,所以不同的redis线程处理不同的消息(不同taskid))。当现实应用中消息和写redis线程是有一个哈希算法的,所以根据map就能够轻松的找到对应的redis写线程。
职责链示例代码:
1 Class Base 2 { 3 Public: 4 Virtual int handle() = 0; 5 }; 6 7 Class Node : public Base 8 { 9 Private: 10 Node* m_nextNode; 11 Public: 12 Bool isIcanHandle(); 13 Virtual int handle() 14 { 15 If(isIcanHandle()) 16 //deal it 17 Else 18 m_nextNode->handle(); 19 } 20 };
同理组合模式也是让对象连成一颗树,叶子节点和非叶子节点分开处理,有请求过来就遍历整棵树,或者从某个节点开始遍历。迭代器模式就是一个迭代器,有前后对象的指针,依次遍历处理。享元模式的核心就是维护一个map,然后遍历一个这个map有没有这类对象,没有就创建一个,有就共用之前的。
总结
设计模式可以说得上是程序员修炼中最难的一环,讲究的是思维模式的变化。如果是某一门编程语言不会,按部就班把这门语言对应的语法过一篇就差不多,但是何时用何种设计模式都是没有一个定论的。有时候代码看着差不多,实际上已经差之千里了。所以考究一段代码是否设计合理,只需要判断是否符合设计原则即可,而不是更要生搬硬套它符合什么模式。
正如GOF所说,这不是一本看一遍就能够束之高阁的书,设计模式的探究将会伴随着程序员的一生,所以这仅仅是个开始,文章的理解可能不是很深刻,如有错漏,还望指正,不胜感激。