设计模式 - 适配器模式

来源:互联网 发布:免费香港域名 编辑:程序博客网 时间:2024/04/27 18:55

从这个文章开始,我们讲结构型模式。结构型模式涉及到如何组合类和对象以获得更大的结构。

先从适配器模式(Adapter)开始。

现实生活中,适配器的情况还是挺多的。比如我去年买了个台湾版HTC手机,但是在杭州用不了,因为这个手机的充电器的插头跟插座不匹配,然后我去电脑市场花了5快钱买了个转接头,现在就可以用了。其实这也是一个适配器的例子。这里面,现有的插座就是Adeptee,转接头就是Adapter。也就是说我们通过一个Adapter(转接头)来使用现有的插座。如果不使用转接头,那么我们就得改造现有的插座,这个太坑爹了。所以说,适配器模式就是以比较小的代价来使用现有的资源。

 

意图

将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

 

结构图

 

类适配器

对象适配器

适配器模式可以分为两种:类适配器和对象适配器。

从上面的图中就可以看出,类适配器继承了Adaptee,而对象适配器组合了Adaptee(也就是说在Adapter的对象里面保存了一个Adaptee的对象)。

 

我这里有个实际的例子,大概8年前,我在公司里面碰到过一个事情。当时的情况大概是这样的:公司有两套加密的代码,一套比较新,是基于接口编程的(当时用了DES加密),另外一套估计已经年代久远了(是公司自己实现的一套加密算法),公司里已经没有人知道什么时候写的了,但是一直在用。

公司自己的加密算法,名字这里就用SKY代替(尽管已经不在那家公司了,但是还是保密一下吧)。这个代码已经long long ago了,但是一直在用,还很好用,呵呵。

class CSKYEncryption{public:void SKYEncrypt(wchar_t* pszFileName){std::cout << "do SKY encryption\n";}};

然后后面一个大哥不知道什么原因封装了另外一套加密算法代码(估计是因为这哥们也没考虑已经存在的加密类,呵呵),代码大概样子如下:

class CEncryption{public:virtual void Encrypt(wchar_t* pszFileName) = 0;};class CEncryption_DES: public CEncryption{public:virtual void Encrypt(wchar_t* pszFileName){std::cout << "do DES encryption\n";}};class CEncryptionUtil{public:void EncryptFile(wchar_t* pszFileName){CEncryption* encryption = CreateAlgorithm();encryption->Encrypt(pszFileName);delete encryption;}protected:virtual CEncryption* CreateAlgorithm(){return new CEncryption_DES();}};

他当时是这么写的,看上去这位大哥比较厉害点(他当时已经离职了)。其实我当时并没有过多接触设计模式,所以也不是很理解他这段代码,直到大概4年前我开始接触设计模式的时候,我才知道原来这位大哥在CEncryptionUtil里面用了工厂方法模式。 

由于产品兼容性的问题,领导叫我在CEncryptionUtil里面要支持公司自己的加密算法:CSKYEncryption。我当时很傻,我是这么做的,

class CSKYEncryptionUtil: public CEncryptionUtil{public:void SKY_EncryptFile(wchar_t* pszFileName){CSKYEncryption* encryption = new CSKYEncryption();encryption->SKYEncrypt(pszFileName);delete encryption;}};

我当时就直接派生了一个类,然后增加了一个函数,这个函数就用来支持公司自己的加密算法。然后在CEncryptionUtil的调用者那里改了代码,来调用这个新增加的函数。就这么交差了。那么这么做有什么问题呢?

1. 这种方法违背了面向接口编程的原则,就是说CSKYEncryptionUtil继承了CEncryptionUtil,然后在里面增加了一个SKY_EncryptFile的函数,而基类CEncryptionUtil并不知道这个函数。那么CEncryptionUtil/CSKYEncryptionUtil的调用者那里就比较难控制,假如调用者那里大量的使用CEncryptionUtil指针作为参数来传递的话,这个就很要命,因为通过CEncryptionUtil无法调用SKY_EncryptFile。唯一能做的就是把CEncrytionUtil指针在某些特定的地方强制转换成CSKYEncryptionUtil指针,然后调用SKYEncrypt()。这个是高难度动作,而且很多时候会出很多问题。非常的危险。

2. 从CEncryptionUtil角度来讲,本来CEncryptionUtil是面向CEncryption接口来编程的,但是现在直接耦合了CSKYEncryption。这个又给程序带来了复杂性。

当然如果系统本身并不复杂的话,上面的2个问题其实也不是什么大问题,但是当一个系统结构比较复杂的时候,那么这2个问题就是大问题了。

那么怎么来提高代码质量呢?ok,我们可以考虑适配器模式。首先我们来演示一下类适配器。

 

类适配器

CEncryption和CSKYEncryption是2个已经存在的类,然后他们的接口不一致,导致使用的时候带来种种困难。那么我们可以通过适配器来转换一下,从而使得调用者可以通过一个标准的接口来调用CSKYEncryption这个旧类。

先给出类图:

这里增加一个新的类CSKYEncryptionAdapter,这个类是CEncryption的子类,同时它也继承于CSKYEncyrption。

class CSKYEncryptionAdapter: public CEncryption, private CSKYEncryption{public:virtual void Encrypt(wchar_t* pszFileName){this->SKYEncrypt(pszFileName);}};

从上面的代码可以看出,CSKYEncryptionAdapter实现了CEncryption的接口Encrypt,这个函数里面其实就是转接调用旧类CSKYEncryption的函数SKYEncrypt。也就是说将旧的接口适配到新的接口,从而使得新的系统可以方便地调用旧的接口。那么CEnryptionUtil怎么调用呢?针对上面的例子,我们只需要派生一个CEncryptionUtil的子类CSKYEncrytionUtil,实现下面代码:

class CSKYEncryptionUtil: public CEncryptionUtil{protected:virtual CEncryption* CreateAlgorithm(){return new CSKYEncryptionAdapter();}};

和前面的版本比较,这里我们实现了CEncryptionUtil的接口CreateAlgorithm,而无需创建一个新函数。那么CEncryptionUtil的调用者可以继续通过CEncryptionUtil指针来调用相应的派生类的功能(只需要做一个小小的改动,就是创建一个指向CSKYEncryptionUtil的CEncryptionUtil指针)。而且CEncryptionUtil还是基于CEncryption接口的,而不需要直接耦合CSKYEncryption。
使用类适配器模式可以将2个不同接口的类放在一起工作。和我8年前的办法相比较,类适配器明显高一个档次。 那么再考虑一个问题,假如CSKYEncryption也有子类呢?这里假设是一个叫做CSKYEncryption2的类。然后CEncryptionUtil想使用CSKYEncryption2,那么类适配器应该怎么做呢?这里就需要再派生一个CEncryption的类(一个新的适配器),假如CSKYEncryptionAdapter2,然后实现代码:

class CSKYEncryptionAdapter2: public CEncryption, private CSKYEncryption2{public:virtual void Encrypt(wchar_t* pszFileName){this->SKYEncrypt(pszFileName);}};

然后再创建一个CEncryptionUtil的子类,来生成一个CSKYEncryptionAdapter2的对象(类似于CSKYEncryptionUtil),好麻烦啊,多加了2个类。那么也就是说假如旧的类有很多子类的话,类适配器就会非常的麻烦。有没有更好的办法呢?一开始我们就讲了适配器模式有两种:类适配器和对象适配器。接下来我们就介绍一下对象适配器。

 

对象适配器

比较类适配器和对象适配器的结构图,唯一的区别就是对象适配器里面的适配器并不是继承于旧的类,而是组合了旧的类。


 

 直接给出代码:

#pragma once#include <iostream>class CSKYEncryption{public:virtual void SKYEncrypt(wchar_t* pszFileName){std::cout << "do SKY encryption\n";}};class CEncryption{public:virtual void Encrypt(wchar_t* pszFileName) = 0;};class CEncryption_DES: public CEncryption{public:virtual void Encrypt(wchar_t* pszFileName){std::cout << "do DES encryption\n";}};class CEncryptionUtil{public:void EncryptFile(wchar_t* pszFileName){CEncryption* encryption = CreateAlgorithm();encryption->Encrypt(pszFileName);delete encryption;}protected:virtual CEncryption* CreateAlgorithm(){return new CEncryption_DES();}};class CSKYEncryptionAdapter: public CEncryption{public:CSKYEncryptionAdapter(CSKYEncryption* enc){_enc = enc;}~CSKYEncryptionAdapter(){delete _enc;}virtual void Encrypt(wchar_t* pszFileName){_enc->SKYEncrypt(pszFileName);}private:CSKYEncryption* _enc;};class CSKYEncryptionUtil: public CEncryptionUtil{protected:virtual CEncryption* CreateAlgorithm(){CSKYEncryption* enc = new CSKYEncryption();return new CSKYEncryptionAdapter(enc);}};

关键在于CSKYEncryptionAdapter,和类适配器版本比较,区别就是不再继承CSKYEncryption,取而代之的是在里面放了一个CSKYEncryption指针变量,然后Encrypt()里面调用这个指针的SKYEncrypt函数。这个指针是在构造函数里面设置的,也就是说CSKYEncryptionAdapter的调用者(CSKYEncryptionUtil)负责来创建一个CSKYEncryption对象并且传递给CSKYEncryptionAdapter对象。那么这么做有什么好处呢?参考类适配器里面最后的那个问题,假如CSKYEncrytion有子类的时候,并且CEncryptionUtil想使用那个子类,那么在CSKYEncryptionUtil里面只需要创建一个CSKYEncryption的子类的对象,并且传递给适配器对象(CSKYEncryptionAdapter)就可以了,而不需要像类适配器那样需要创建一个新适配器了。当然对于不同的CSKYEncryption(旧类)的子类,可能需要创建不同的CSKYEncryptionUtil子类,因为要创建不同的CSKYEncrytion子类对象。这个也挺麻烦,当然我们可以对CEncryptionUtil进行一定的改装来解决这个问题,这个问题就不在这里探讨了。相对于类适配器,对象适配器也有问题,就是对象适配器无法重新定义旧类的行为,而类适配器可以,因为继承了旧类。

 

综合类适配器和对象适配器,我们可以得出以下结论:

类适配器

1. 当旧类有很多子类的时候,就不太适合;

2. 可以重定义旧类(Adaptee)的部分行为;

3. 不需要额外的指针以间接得到Adaptee。

 

对象适配器

1. 允许一个Adapter与多个Adapteee,即Adaptee本身以及它的所有子类(如果有子类的话)一起工作。其实也就是通过设置Adapter内部Adaptee指针来实现的。就像上面的例子,我们只需要一个适配器CSKYEncryptionAdapter就可以适配CSKYEncrytion以及它所有的子类;

2. 重定义Adaptee的行为比较困难。这需要生成Adaptee的子类并且引用这个子类而不是引用Adaptee本身。这样也就失去了第一个优点,不值得考虑。

类适配器和对象适配器各有各的用处,看具体情况来决定使用哪一种。总之,当我们发现系统里面已经存在的类由于接口不一致而不能很好的工作的时候就可以考虑适配器模式,或许可以带来一定的帮助。好了,讲完了,如有不对的地方,欢迎指出。大家共同学习。

 

原创粉丝点击