“单一职责”模式:
在软件组件的设计中,如果责任划分的不清晰,使用继承得到的结果往往是随着需求的变化,子类急剧膨胀,同时充斥着重复代码,这时候的关键是划清责任。
典型模式
• Decorator
• Bridge
模式定义
动态(组合)地给一个对象增加一些额外的职责。就增加功能而言,Decorator模式比生成子类(继承)更为灵活(消除重复代码 & 减少子类个数)。 ——《设计模式》GoF
动机
一般有两种方式给类或对象添加行为:
- 继承机制,通过继承一个现有类可以使得子类在拥有自身方法同时还拥有父类的方法,但该方式是静态的,用户不能控制增加行为的方式和时机,而且随着扩展功能增多,子类也增多,就会导致类膨胀。
- 关联机制,即将一个类的对象嵌入另一个对象中,由另一个对象来决定是否调用嵌入对象的行为以便扩展自己的行为,我们称这个嵌入的对象为装饰器(Decorator)
装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展而不是给整个类添加一些功能。
来看一个设计场景,在设计IO库的时候会涉及到流操作,流有分为文件流,网络流,内存流,都有类似的操作,比如读,定位,写等操作。很容易想到会用一个抽象基类,定义操作接口,在子类中具体实现。
1 //业务操作 2 class Stream{ 3 public: 4 virtual char Read(int number)=0; 5 virtual void Seek(int position)=0; 6 virtual void Write(char data)=0; 7 8 virtual ~Stream(){} 9 }; 10 11 //主体类 12 class FileStream: public Stream{ 13 public: 14 virtual char Read(int number){ 15 //读文件流 16 } 17 virtual void Seek(int position){ 18 //定位文件流 19 } 20 virtual void Write(char data){ 21 //写文件流 22 } 23 24 }; 25 26 class NetworkStream :public Stream{ 27 public: 28 virtual char Read(int number){ 29 //读网络流 30 } 31 virtual void Seek(int position){ 32 //定位网络流 33 } 34 virtual void Write(char data){ 35 //写网络流 36 } 37 38 }; 39 40 class MemoryStream :public Stream{ 41 public: 42 virtual char Read(int number){ 43 //读内存流 44 } 45 virtual void Seek(int position){ 46 //定位内存流 47 } 48 virtual void Write(char data){ 49 //写内存流 50 } 51 52 };
此时需要对流的功能进行扩展,比如进行加密,加密操作一般会在读写的过程中进行。所以就会有以下的扩展出来的类:
1 //扩展操作 2 class CryptoFileStream :public FileStream{ 3 public: 4 virtual char Read(int number){ 5 6 //额外的加密操作... 7 FileStream::Read(number);//读文件流 8 9 } 10 virtual void Seek(int position){ 11 //额外的加密操作... 12 FileStream::Seek(position);//定位文件流 13 //额外的加密操作... 14 } 15 virtual void Write(byte data){ 16 //额外的加密操作... 17 FileStream::Write(data);//写文件流 18 //额外的加密操作... 19 } 20 }; 21 22 class CryptoNetworkStream : :public NetworkStream{ 23 public: 24 virtual char Read(int number){ 25 26 //额外的加密操作... 27 NetworkStream::Read(number);//读网络流 28 } 29 virtual void Seek(int position){ 30 //额外的加密操作... 31 NetworkStream::Seek(position);//定位网络流 32 //额外的加密操作... 33 } 34 virtual void Write(byte data){ 35 //额外的加密操作... 36 NetworkStream::Write(data);//写网络流 37 //额外的加密操作... 38 } 39 }; 40 41 class CryptoMemoryStream : public MemoryStream{ 42 public: 43 virtual char Read(int number){ 44 45 //额外的加密操作... 46 MemoryStream::Read(number);//读内存流 47 } 48 virtual void Seek(int position){ 49 //额外的加密操作... 50 MemoryStream::Seek(position);//定位内存流 51 //额外的加密操作... 52 } 53 virtual void Write(byte data){ 54 //额外的加密操作... 55 MemoryStream::Write(data);//写内存流 56 //额外的加密操作... 57 } 58 };
在上面代码中,加密的操作是一样的,只是read、write、seek流的操作不一样
如果还要增加缓存操作,对于每种流又会增加一个针对缓存功能的类。
class BufferedFileStream : public FileStream{ //... }; class BufferedNetworkStream : public NetworkStream{ //... }; class BufferedMemoryStream : public MemoryStream{ //... }
同理,如果要使流支持在读写的时候进行缓存加密,又会增加一个类
1 class CryptoBufferedFileStream :public FileStream{ 2 public: 3 virtual char Read(int number){ 4 5 //额外的加密操作... 6 //额外的缓冲操作... 7 FileStream::Read(number);//读文件流 8 } 9 virtual void Seek(int position){ 10 //额外的加密操作... 11 //额外的缓冲操作... 12 FileStream::Seek(position);//定位文件流 13 //额外的加密操作... 14 //额外的缓冲操作... 15 } 16 virtual void Write(byte data){ 17 //额外的加密操作... 18 //额外的缓冲操作... 19 FileStream::Write(data);//写文件流 20 //额外的加密操作... 21 //额外的缓冲操作... 22 } 23 };
对于以上也就是说,在操作文件流,且不需要扩展功能的读写定位操作时,就使用FileStream类就可以,如果要扩展加密功能,或者要扩展缓存功能,又或者既要加密又要缓存,就得再分别调用对应的类,这些都是通过增加派生类(静态地)来扩展功能。
可以看到一旦功能发生扩展,就会衍生出非常的多的子类。
在上面代码中,不同流的read,write,seek虽然实现细节不同,但是作为接口的输入输出是一样的,就是对接口加密操作的实现是一样的。现在开始优化一下,首先设计原则中,组合优于继承,将具体的流基类作为组件而不是继承:
1 //扩展操作 2 class CryptoFileStream :public FileStream{ 3 public: 4 ... 5 } 6 7 //将继承转为组合 8 class CryptoFileStream{ 9 public: 10 FileStream* stream; 11 virtual char Read(int number){ 12 //额外的加密操作 13 stream->Read(number); 14 } 15 ... 16 } 17 18 class CryptoNetworkStream{ 19 public: 20 NetworkStream* stream; 21 virtual char Read(int number){ 22 //额外的加密操作 23 stream->Read(number); 24 } 25 ... 26 } 27 28 class CryptoMemoryStream{ 29 public: 30 MemoryStream* stream; 31 virtual char Read(int number){ 32 //额外的加密操作 33 stream->Read(number); 34 } 35 ... 36 }
看到第10行,20行,30行。一般说如果类中的成员变量是拥有继承于同一基类(Stream)的子类(FileStream, NetworkStream, MemoryStream),我们将这个变量声明为基类类型(Stream)即可,在未来/运行时再与具体的子类绑定。
class CryptoFileStream { public: Stream*stream; // = new FileStream, 运行时再绑定 virtual char Read(int number){
//额外的加密操作
stream->Read(number);
} }; class CryptoNetworkStream { public: Stream*stream; // = new NetworkStream, 运行时再绑定 ... }; class CryptoMemeoryStream { public: Stream*stream; // = new MemoryStream, 运行时再绑定 ... };
现在可以发现这个三个子类都拥有同一个成员变量stream(在编译时一样,运行时不一样,将需求和变化延迟到运行时再实现),
那么这三个类中的Read,Write,Seek在编译期间也就一样,都是Stream->Read(),只有运行时,才会绑定为具体流子类的方法 -- FileStream->Read(),也就是说这三个类在编译期间是一模一样的。消除这三个重复的类。
改进如下:
1 //将三个具体的流类抽象为一个加密类,该类就完成不同流的加密功能 2 class CryptoStream { 3 public: 4 virtual char Read(int number){ 5 //额外的加密操作... 6 stream->Read(number); 7 }
Stream* Stream; 8 };
注意到,CryptoStream已经没有继承FileStream,而是将其转为Stream基类做为成员函数,那剩下来的虚函数 virtual char Read(int number)怎么办,它是继承于哪个类的虚函数,光靠组合没法实现多态,所以还得继承Stream,保证与它所装饰的Stream的接口一致性。改进如下:
1 //业务操作 2 class Stream{ 3 4 public: 5 virtual char Read(int number)=0; 6 virtual void Seek(int position)=0; 7 virtual void Write(char data)=0; 8 9 virtual ~Stream(){} 10 }; 11 12 //主体类 13 class FileStream: public Stream{ 14 public: 15 virtual char Read(int number){ 16 //读文件流 17 } 18 virtual void Seek(int position){ 19 //定位文件流 20 } 21 virtual void Write(char data){ 22 //写文件流 23 } 24 }; 25 26 class NetworkStream :public Stream{ 27 public: 28 virtual char Read(int number){ 29 //读网络流 30 } 31 virtual void Seek(int position){ 32 //定位网络流 33 } 34 virtual void Write(char data){ 35 //写网络流 36 } 37 }; 38 39 class MemoryStream :public Stream{ 40 public: 41 virtual char Read(int number){ 42 //读内存流 43 } 44 virtual void Seek(int position){ 45 //定位内存流 46 } 47 virtual void Write(char data){ 48 //写内存流 49 } 50 }; 51 52 //扩展操作 53 class CryptoStream: public Stream { 54 Stream* stream;//... 55 56 public: 57 CryptoStream(Stream* stm):stream(stm){ 58 } 59 60 virtual char Read(int number){ 61 //额外的加密操作... 62 stream->Read(number);//读文件流 63 } 64 virtual void Seek(int position){ 65 //额外的加密操作... 66 stream::Seek(position);//定位文件流 67 //额外的加密操作... 68 } 69 virtual void Write(byte data){ 70 //额外的加密操作... 71 stream::Write(data);//写文件流 72 //额外的加密操作... 73 } 74 }; 75 76 class BufferedStream : public Stream{ 77 Stream* stream;//... 78 public: 79 BufferedStream(Stream* stm):stream(stm){ 80 } 81 //... 82 }; 83 84 void Process(){ 85 //运行时装配 86 FileStream* s1=new FileStream(); 87 CryptoStream* s2=new CryptoStream(s1); //加密 88 BufferedStream* s3=new BufferedStream(s1); //缓存 89 BufferedStream* s4=new BufferedStream(s2); //即加密又缓存 90 91 }
这时的代码相对于第一版本已经好很多了。再看看还有什么改进的地方:
注意BufferedStream和CryptoStream这两个类,含有相同的成员变量Stream* stream。
重构中提到:如果某个类的多个子类含有相同字段,应该将字段的层次往上提。
一般有两种提法:
一是提到基类Stream中,但是FileStream继承于Stream,FileStream又不需要Stream* stream字段。故不合适
再是设计一个中间类DecoratorStream来包含Stream* Stream字段。
1 #include <iostream> 2 3 using namespace std; 4 5 class Stream { 6 public: 7 virtual void Read(int number) = 0; 8 virtual void Seek(int position) = 0; 9 virtual void Write(char data) = 0; 10 }; 11 12 class FileStream : public Stream { 13 public: 14 virtual void Read(int number) { cout << "FileStream Read\n"; } 15 virtual void Seek(int position) { cout << "FileStream Seek\n"; } 16 virtual void Write(char data) { cout << "FileStream Write\n"; } 17 }; 18 19 class NetStream : public Stream { 20 public: 21 virtual void Read(int number) {} 22 virtual void Seek(int position) {} 23 virtual void Write(char data) {} 24 }; 25 26 class MemoryStream : public Stream { 27 public: 28 virtual void Read(int number) {} 29 virtual void Seek(int position) {} 30 virtual void Write(char data) {} 31 }; 32 33 //扩展操作 34 35 class DecoratorStream : public Stream { 36 protected: 37 Stream* stream; //... 38 DecoratorStream(Stream* stm) : stream(stm) {} 39 }; 40 41 class CryptoStream : public DecoratorStream { 42 public: 43 CryptoStream(Stream* stm) : DecoratorStream(stm) {} 44 45 virtual void Read(int number) { 46 //额外的加密操作... 47 cout << "Crypto Read...\n"; 48 DecoratorStream::stream->Read(number); //读文件流 49 } 50 virtual void Seek(int position) { 51 //额外的加密操作... 52 cout << "Crypto Seek...\n"; 53 DecoratorStream::stream->Seek(position); //定位文件流 54 } 55 virtual void Write(char data) { 56 //额外的加密操作... 57 cout << "Crypto Write...\n"; 58 DecoratorStream::stream->Write(data); //写文件流 59 } 60 }; 61 62 class BufferedStream : public DecoratorStream { 63 public: 64 BufferedStream(Stream* stm) : DecoratorStream(stm) {} 65 66 virtual void Read(int number) { 67 //额外的缓存操作... 68 cout << "Buffer Read...\n"; 69 DecoratorStream::stream->Read(number); //读文件流 70 } 71 virtual void Seek(int position) { 72 //额外的缓存操作... 73 cout << "Buffer Seek...\n"; 74 DecoratorStream::stream->Seek(position); //定位文件流 75 } 76 virtual void Write(char data) { 77 //额外的缓存操作... 78 cout << "Buffer Write...\n"; 79 DecoratorStream::stream->Write(data); //写文件流 80 } 81 }; 82 83 int main() { 84 //运行时装配 85 FileStream* s1 = new FileStream(); 86 87 cout << "============Crypto==========\n"; 88 CryptoStream* s2 = new CryptoStream(s1); 89 s2->Read(0); 90 s2->Seek(0); 91 s2->Write('a'); 92 93 cout << "============Buffer==========\n"; 94 BufferedStream* s3 = new BufferedStream(s1); 95 s3->Read(0); 96 s3->Seek(0); 97 s3->Write('a'); 98 99 cout << "============Crypto & Buffer=========\n"; 100 BufferedStream* s4 = new BufferedStream(s2); 101 s4->Read(0); 102 s4->Seek(0); 103 s4->Write('a'); 104 105 return 0; 106 }
Stream:抽象流基类,定义了对象接口,不会实现具体的行为。
FileStream,NetworkStream,MemoryStream定义了具体的Stream,继承于Stream,重写了虚函数;
DecoratorStream:它是一个中间类,维护一个指向Stream对象的指针,指向需要被装饰的对象,并在它的子类CryptoStream和BufferStream中实现与Stream接口一致的接口。
UML类图
Component:定义一个对象接口,可以给对象动态得添加职责,该接口定义为纯虚函数。相当于Stream,是稳定的部分。
ConcreteComponent:定义一个具体的Component,继承于Component,重写Component的虚函数。相当于FileStream, NetworkStream等,该部分是变化的。
Decorator:维持一个指向Component对象的指针,该指针指向需要被装饰的对象,并定义一个与Component接口一致的接口。相当于DecoratorStream, 该部分是稳定的。
ConcreteDecorator:想组件添加职责。相当于CryptoStream, BufferStream是变化的。
代码实现:
1 #include <iostream> 2 3 using namespace std; 4 5 class Component { 6 public: 7 virtual void Operation() = 0; 8 }; 9 10 class ConcreteComponent : public Component { 11 public: 12 void Operation() { cout << "I am not decorator ConcreteComponent\n"; } 13 }; 14 15 class Decorator : public Component { 16 public: 17 Decorator(Component* pComponent) : m_pComponentObj(pComponent) {} 18 void Operation() { 19 if (!m_pComponentObj) { 20 m_pComponentObj->Operation(); 21 } 22 } 23 24 protected: 25 Component* m_pComponentObj; 26 }; 27 28 class ConcreteDectoratorA : public Decorator { 29 public: 30 ConcreteDectoratorA(Component* pDecorator) : Decorator(pDecorator) {} 31 void Operation() { 32 AddBehavior(); 33 Decorator::Operation(); 34 } 35 void AddBehavior() { cout << "This is added behavior A\n"; } 36 }; 37 38 class ConcreteDectoratorB : public Decorator { 39 public: 40 ConcreteDectoratorB(Component* pDecorator) : Decorator(pDecorator) {} 41 void Operation() { 42 AddBehavior(); 43 Decorator::Operation(); 44 } 45 void AddBehavior() { cout << "This is added behavior B\n"; } 46 }; 47 48 int main() { 49 Component* pComponent = new ConcreteComponent(); 50 pComponent->Operation(); 51 cout << "============================\n"; 52 53 Decorator* pDecoratorA = new ConcreteDectoratorA(pComponent); 54 pDecoratorA->Operation(); 55 cout << "============================\n"; 56 57 Decorator* pDecoratorB = new ConcreteDectoratorB(pComponent); 58 pDecoratorB->Operation(); 59 cout << "============================\n"; 60 61 pComponent->Operation(); 62 cout << "============================\n"; 63 64 delete pDecoratorB; 65 pDecoratorB = nullptr; 66 delete pDecoratorA; 67 pDecoratorA = nullptr; 68 delete pComponent; 69 pComponent = nullptr; 70 return 0; 71 }
注意事项
- 接口的一致性:装饰对象的接口必须与它所装饰的Component的接口是一致的,因此,所有的ConcreteDecorator类必须有一个公共的父类;这样对于用户来说,就是统一的接口;
- 省略抽象的Decorator类:当仅需要添加一个职责时,比如只需要扩展加密功能,或者只需要扩展缓存功能。那就没有必要定义抽象Decorator类。因为我们常常要处理,现存的类层次结构而不是设计一个新系统,这时可以把Decorator向Component转发请求的职责合并到ConcreteDecorator中;
- 保持Component类的简单性:为了保证接口的一致性,组件和装饰必须要有一个公共的Component类,所以保持这个Component类的简单性是非常重要的,所以,这个Component类应该集中于定义接口而不是存储数据。对数据表示的定义应延迟到子类中,否则Component类会变得过于复杂和臃肿,因而难以大量使用。赋予Component类太多的功能,也使得具体的子类有一些它们它们不需要的功能大大增大;
实现要点
- Component类在Decorator模式中充当抽象接口的角色,不应该去实现具体的行为。而且Decorator类对于Component类应该透明,换言之Component类无需知道Decorator类,Decorator类是从外部来扩展Component类的功能;
- Decorator类在接口上表现为“is-a”Component的继承关系,即Decorator类继承了Component类所具有的接口。但在实现上又表现为“has-a”Component的组合关系,即Decorator类又使用了另外一个Component类。我们可以使用一个或者多个Decorator对象来“装饰”一个Component对象,且装饰后的对象仍然是一个Component对象;
- Decortor模式并非解决“多子类衍生的多继承”问题,Decorator模式的应用要点在于解决“主体类在多个方向上的扩展功能”——是为“装饰”的含义;比如说Stream和“ 文件流,网络流,内存流 ”,它们之间是继承的关系,但是加密流,缓存流和文件流等之间不是is-a的关系而是属于多方向上的扩展。
- 对于Decorator模式在实际中的运用可以很灵活。如果只有一个ConcreteComponent类而没有抽象的Component类,那么Decorator类可以是ConcreteComponent的一个子类。如果只有一个ConcreteDecorator类,那么就没有必要建立一个单独的Decorator类,而可以把Decorator和ConcreteDecorator的责任合并成一个类。
- Decorator模式的优点是提供了比继承更加灵活的扩展,通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合;
- 由于使用装饰模式,可以比使用继承关系需要较少数目的类。使用较少的类,当然使设计比较易于进行。但是,在另一方面,使用装饰模式会产生比使用继承关系更多的对象。更多的对象会使得查错变得困难,特别是这些对象看上去都很相像。
参考:
李建忠 -- C++设计模式(装饰模式)