C++设计模式_06_Decorator 装饰模式

news2024/11/24 12:24:05

本篇将会介绍Decorator 装饰模式,它是属于一个新的类别,按照C++设计模式_03_模板方法Template Method中介绍的划分为“单一职责”模式
“单一职责”模式讲的是在软件组件的设计中,如果责任划分的不清晰,使用继承得到的结果往往是随着需求的变化,子类急剧膨胀,同时充斥着重复代码,这时候的关键是划清责任。

典型的模式包括:Decorator Bridge。这是因为这两种模式在责任的问题上表现得特别突出,但不意味着其他模式没有责任问题。
本篇主要介绍Decorator 装饰模式

文章目录

  • 1. 代码演示Decorator 装饰模式
    • 1.1 基于继承的常规思维处理
    • 1.2 基于组合关系的重构优化
    • 1.3 采用Decorator 装饰模式的实现
  • 2. 动机(Motivation)
  • 3. 模式定义
  • 4. 结构(Structure)
  • 5. 要点总结
  • 6. 其他参考博文

1. 代码演示Decorator 装饰模式

首先结合实际场景和代码进行分析
设计场景:设计一些IO库,涉及一些流操作,针对流的操作,我们具有很多需求,比如文件流、网络流、内存流等,也有对流进行加密,进行缓存等操作。

1.1 基于继承的常规思维处理

首先我们可能会想到流的设计首先需要一个基类,基类有一些例如:Read()、Seek()、Write()等方法的公共操作,作为纯虚函数,放到Stream基类中。文件流FileStream继承自Stream基类,override Read()、Seek()、Write()等虚函数。网络流NetworkStream、内存流MemoryStream操作也是类似的

当我们进行加密操作时可以发现,首先我们需要对流有个主体的操作才能加密,我们对其中的文件流进行加密,首先去继承文件流FileStream,在Read()中进行额外的加密操作,调用基类的方法FileStream::Read(number);,Seek()、Write()方法也是在其前面做额外的加密操作。

上述过程是对文件流进行加密操作,对网络流、内存流也有加密需求,也需要重复上面在文件流加密中的动作,我们可以发现加密操作是一样的,只是流的读取操作等不一样。

相应的对流的缓冲操作BufferedFileStream,也需要考虑文件流、网络流、内存流的操作,代码并未详细写

对文件流既加密又缓冲的双重操作CryptoBufferedFileStream,此处是继承了FileStream,当然有些朋友可以直接在这里继承一个CryptoFileStream也是可以的。此处进行额外的加密和缓冲操作。

//业务操作
class Stream{
publicvirtual 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){
        //写内存流
    }
    
};

//扩展操作
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 CryptoMemoryStream : public MemoryStream{
public:
    virtual char Read(int number){
        
        //额外的加密操作...
        MemoryStream::Read(number);//读内存流
    }
    virtual void Seek(int position){
        //额外的加密操作...
        MemoryStream::Seek(position);//定位内存流
        //额外的加密操作...
    }
    virtual void Write(byte data){
        //额外的加密操作...
        MemoryStream::Write(data);//写内存流
        //额外的加密操作...
    }
};

class BufferedFileStream : public FileStream{
    //...
};

class BufferedNetworkStream : public NetworkStream{
    //...
};

class BufferedMemoryStream : public MemoryStream{
    //...
}




class CryptoBufferedFileStream :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);//写文件流
        //额外的加密操作...
        //额外的缓冲操作...
    }
};



void Process(){

        //编译时装配
    CryptoFileStream *fs1 = new CryptoFileStream();

    BufferedFileStream *fs2 = new BufferedFileStream();

    CryptoBufferedFileStream *fs3 =new CryptoBufferedFileStream();

}

针对上述代码,我们来分析是否存在问题。
整理设计类图结构,Stream是基类,FileStream、NetworkStream、MemoryStream均继承自Syream,并对Read()、Seek()、Write()等进行了override,并且对各个基类进行了CryptoFileStream、BufferedFileStream和CryptoBufferedFileStream。

在这里插入图片描述

大家可以考虑下,当存在n个流类型的需求时,这个类的规模有多少呢?

Stream记作1,FileStream、NetworkStream、MemoryStream…等n种流类型,对于每个流类型有CryptoFileStream、BufferedFileStream和CryptoBufferedFileStream…等m种操作。总共加起来的个数为:1+n+(m!/2),(很多人很容易想到其为1+nm,但实际在操作中会有既加密又等组合的情况,实际就是n(m+(m-1)+(m-2)…+1),数学中对应的就是m的阶乘除以2)。这个类的规模就会变得十分大。

再研究上面的代码,发现对于加密操作,不管是哪个类方法的加密方法,都是一样的,大量重复的代码就造成了代码冗余。

1.2 基于组合关系的重构优化

为了消除重复带来的代码冗余,对代码进行重构。

利用组合关系替代继承关系(设计原则中也有提到组合由于继承),在重构中的规则:当一个变量的声明类型都是某一个类型的子类的时候,那么在声明时就将其声明为该类型

例如上面代码转化组合关系时,从某一个类的FileStream* stream,改定义为Stream* stream;,利用多态在运行时定义为子类型既可以(实现方法:Stream* stream = new FileStream();),这也就使得编译时的东西变为运行时的东西,这里可以悟出一个设计模式的真谛就是“编译时一样,运行时不一样”

经过对代码的重构,就将class CryptoFileStream :public FileStream、class CryptoNetworkStream : :public NetworkStream和class CryptoMemoryStream : public MemoryStream的3段重复性代码重构为以下的一段代码。

class CryptoStream: public Stream {
    
    Stream* stream;//...

public:
    CryptoStream(Stream* stm):stream(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);//写文件流
        //额外的加密操作...
    }
};

以上代码中Read()、Seek()、Write()均采用虚函数的方式,这是因为遵循流的规范,才能使用虚函数,因此class CryptoStream需要继承public Stream,是完善Read()、Seek()、Write()虚函数的接口规范,也就是Stream基类定义了CryptoStream的接口规范。

class CryptoStream: public Stream {
virtual char Read(int number){...}
virtual void Seek(int position){...}
virtual void Write(byte data){...}
}

这时就有一个特别有意思的变化,既有一个Stream基类,又有Stream* stream的字段。

同样的道理class BufferedStream也可以重构为以下代码

class BufferedStream : public Stream{
    
    Stream* stream;//...
    
public:
    BufferedStream(Stream* stm):stream(stm){
        
    }
    //...
};

写到此处,最初代码中的问题已经得到了极大的缓解,真正使用的时候。

void Process(){

    //运行时装配
    FileStream* s1=new FileStream();
    CryptoStream* s2=new CryptoStream(s1);
    
    BufferedStream* s3=new BufferedStream(s1);
    //既加密又缓存  
    BufferedStream* s4=new BufferedStream(s2);       
}

编译时不存在加密文件流,缓存文件流的类,但是运行时可以通过组合的方式把他们装配起来满足需求。

这样最终的代码为:

//业务操作
class Stream{

publicvirtual 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){
        //写内存流
    }
    
};

//扩展操作


class CryptoStream: public Stream {
    
    Stream* stream;//...

public:
    CryptoStream(Stream* stm):stream(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 Stream{
    
    Stream* stream;//...
    
public:
    BufferedStream(Stream* stm):stream(stm){
        
    }
    //...
};

void Process(){

    //运行时装配
    FileStream* s1=new FileStream();
    CryptoStream* s2=new CryptoStream(s1);
    
    BufferedStream* s3=new BufferedStream(s1);
    
    BufferedStream* s4=new BufferedStream(s2);
      
}

到这里问题已经解决的差不多了,这个版本在很多类库中也是比较常见的,这样其实已经OK了

1.3 采用Decorator 装饰模式的实现

如果根据马丁福勒经典意义上的重构理论,重构理论中还有一条:“如果某一个类它有多个子类具有同样的字段时,应该往上提”,Stream* stream;如果放到class Stream,但是在其子类class FileStream并不需要这个字段,FileStream本身就是主体,并不需要Stream* stream字段。

这个时候怎么去做呢?此时需要一个中间类,这个中间类就是class DecoratorStream

class DecoratorStream: public Stream{
protected:
    Stream* stream;//...
    
    DecoratorStream(Stream * stm):stream(stm){
    
    }
    
};

整体代码为:

//业务操作
class Stream{

publicvirtual 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){
        //写内存流
    }
    
};

//扩展操作

class 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);

}

梳理一下,就会发现很巧妙,FileStream、NetworkStream和MemoryStream始终是没有动的,他们是可以单独行驶行为的,但是加密流必须传一个流的对象。这些操作的本质上就是扩展,这就是Decorator 的含义,装饰是附着在其他上的操作。
在这里插入图片描述

Stream基类,FileStream、NetworkStream、MemoryStream作为Stream的子类也是不变,设计了DecoratorStream,CryptoStream和BufferedStream继承自DecoratorStream。这种情况下类的规模就是1+n+m,对比前面1+n+(m!/2)减少了很多。

对两者进行分析发现:出现1+n+(m!/2)的规模是由于重复代码的多次使用,继承的不良使用,我们现在想一下CryptoFileStream和BufferedFileStream一定要继承FileStream来完成加密和缓存操作吗?不是的,组合更好,class DecoratorStream: public Stream{ protected: Stream* stream;//... }中的Stream* stream; 是设计的核心,就是用组合的方式,来引出未来多态的支持。

2. 动机(Motivation)

  • 在某些情况下我们可能会“过度地使用继承来扩展对象的功能”,由于继承为类型引入的静态特质,使得这种扩展方式缺乏灵活性;并且随着子类的增多(扩展功能的增多),各种子类的组合(扩展功能的组合)会导致更多子类的膨胀。
    例如:在decorator1.cpp代码中,class CryptoFileStream中的FileStream::Read(number);就是静态特质,是没有变化的可能性的,这是由继承实现的;而在decorator3.cpp代码中class CryptoStream中的stream->Read(number);中stream是基类的多态指针就是有变化的,这也就是动态特质,这是由组合实现的

  • 如何使“对象功能的扩展”能够根据需要来动态地实现?同时避免“扩展功能的增多”带来的子类膨胀问题?从而使得任何“功能扩展变化”所导致的影响将为最低?

3. 模式定义

动态(组合)地给一个对象增加一些额外的职责。就增加功能而言,Decorator模式比生成子类(继承)更为灵活(消除重复代码 & 减少子类个数)。 ——《设计模式》GoF

4. 结构(Structure)

在这里插入图片描述

上图是《设计模式》GoF中定义的Decorator 装饰模式的设计结构。结合上面的代码看图中最重要的是看其中稳定和变化部分,也就是下图中红框和蓝框框选的部分。
在这里插入图片描述

5. 要点总结

  • 通过采用组合而非继承的手法, Decorator模式实现了在运行时动态扩展对象功能的能力,而且可以根据需要扩展多个功能。避免了使用继承带来的“灵活性差”和“多子类衍生问题”。

  • Decorator类在接口上表现为is-a Component的继承关系,即Decorator类继承了Component类所具有的接口。但在实现上又表现为has-a Component的组合关系,即Decorator类又使用了另外一个Component类。

这是一种同时继承与组合的模式,以后如果看到一个类,它的父类是一个类,例如父类是Stream,又有一个字段是Stream* stream,这个时候就高度怀疑是Decorator 装饰模式。有的时候看不到内部字段,至少可以从类的外部接口进行推测,父类是一个类,构造器的参数也是某一个类型,例如:class CryptoStream: public DecoratorStream {public:CryptoStream(Stream* stm):DecoratorStream(stm){ }}

  • Decorator模式的目的并非解决“多子类衍生的多继承”问题,Decorator模式应用的要点在于解决“主体类在多个方向上的扩展功能”——是为“装饰”的含义。

6. 其他参考博文

Decorator 装饰模式

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1037018.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

HT for Web (Hightopo) 使用心得(2)- 2D 图纸、节点、连线 与基本动画

概括来说,用 HT for Web 做可视化主要分为两部分,也就是 2D 和 3D。这两部分需要单独创建。在它们被创建完成后,我们再把它们集成到一起。 HT for Web 的 2D 部分主要是指 ht.graph.GraphView (简称 GraphView,也就是 2D 图纸)。…

Java项目:SSM的食堂点餐系统

作者主页:Java毕设网 简介:Java领域优质创作者、Java项目、学习资料、技术互助 文末获取源码 一、相关文档 系统中的核心用户是系统管理员,管理员登录后,通过管理员菜单来管理后台系统。主要功能有:个人中心、用户管理…

自动发现、zabbix_proxy代理

自动发现:自己去发现被监控的主机 它能够根据用户事先定义的规则自动添加监控的主机或服务等。 优点 加快Zabbix部署(agent) 简化管理 在快速变化的环境中使用Zabbix,而不需要过度管理 部署自动发现(新机子) rpm -Uvh https://re…

OSI 七层网络协议最全的图

OSI 七层网络协议最全的图 文章出处:https://www.shuzhiduo.com/A/RnJWawowdq/

DINO(ICLR 2023)

DINO(ICLR 2023) DETR with Improved deNoising anchOr box DINO发展: Conditional DETR->DAB-DETR(4D,WH修正) DN-DETR(去噪训练,deNoising 稳定匹配过程) Deformable DETR&…

后端大厂面试-16道面试题

1 java集合类有哪些? List是有序的Collection,使用此接口能够精确的控制每个元素的插入位置,用户能根据索引访问List中元素。常用的实现List的类有LinkedList,ArrayList,Vector,Stack。 ArrayList是容量…

基于同名面片的TLS测站点云配准

1、原理介绍 2、代码介绍 基于C++编写的程序代码如下,其依赖eigen矩阵运算库,在创建工程时包含库目录中使用了相对路径,因此其下载下来直接可以运行,不用单独在设置环境,非常方便。

Java项目:SpringBoot高校宿舍管理系统

作者主页:Java毕设网 简介:Java领域优质创作者、Java项目、学习资料、技术互助 文末获取源码 一、相关文档 宿舍是大学生学习与生活的主要场所之一,宿舍管理是高校学工管理事务中尤为重要的一项。随着我国高校招生规模的进一步扩大&#xff0…

异步回调

Future 设计的初衷:对将来的某个事件的结果进行建模 package com.kuang.future;import com.kuang.pc.C;import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.uti…

ubuntu | 安装NVIDIA套件:驱动、CUDA、cuDNN

CUDA 查看支持最高的cuda版本 nvidia-smiCUDA Version:12.2 区官网下在12.2.x最新的版本即可CUDA Toolkit Archive | NVIDIA Developer 下载安装 wget https://developer.download.nvidia.com/compute/cuda/12.2.2/local_installers/cuda_12.2.2_535.104.05_linux.run sudo…

《开发实战》16 | 缓存设计:缓存可以锦上添花也可以落井下石

不要把 Redis 当作数据库 Redis 的确具有数据持久化功能,可以实现服务重启后数据不丢失。这一点,很容易让我们误认为 Redis 可以作为高性能的 KV 数据库。Redis 的特点是,处理请求很快,但无法保存超过内存大小的数据。第一&#…

leetcode 22. 括号生成

2023.9.24 看到组合两个字,想到了回溯。 大致思路是将所有可能的组合列出来,通过中止条件筛选掉无效的括号。 第一个中止条件:如果右括号数量大于左括号,那括号肯定无效。 第二个中止条件:当左右括号数量相等&#x…

swiper使用

介绍 Swiper(swiper master)是一个第三方的库,可以用来实现移动端、pc端的滑动操作。,swiper应用广泛,使用频率仅次于jquery, 轮播图类排名第一,是网页设计师必备技能,众多耳熟能详的品牌在使用…

Keil 无法烧写程序

问题描述: Keil MDK V5.38 按 F8 键无法烧录程序,提示: Error: Flash Download failed - "Cortex-M7", No Algorithm found for: 08000000H - 080013D3H 解决办法: Debug 工具改为:ST-Link Debugger Debug 的 Conne…

mac怎么把两张图片拼在一起

mac怎么把两张图片拼在一起?在如今的生活中,喜欢摄影的朋友们越来越多。拍照已经成为我们的一种习惯,因为当我们遇到美景或迷人的人物时,总是忍不住按下快门,将它们定格。随着时间的推移,我们渐渐发现自己的…

[Java | Web] JavaWeb——JSON与AJAX简介

目录 一、JSON 简介 1、什么是 JSON 2、JSON 的定义和访问 3、JSON 在 JS 中两种常用的转换方法 4、JSON 在 Java 中的使用 5、匿名内部类 二、AJAX 简介 1、什么是 AJAX 2、原生 JS 的 AJAX 请求示例 3、JQuery 中的 AJAX 请求 一、JSON 简介 1、什么是 JSON JSON…

Elasticsearch:什么是向量和向量存储数据库,我们为什么关心?

Elasticsearch 从 7.3 版本开始支持向量搜索。从 8.0 开始支持带有 HNSW 的 ANN 向量搜索。目前 Elasticsearch 已经是全球下载量最多的向量数据库。它允许使用密集向量和向量比较来搜索文档。 矢量搜索在人工智能和机器学习领域有许多重要的应用。 有效存储和检索向量的数据库…

马尔可夫链预测举例——钢琴销售的存贮策略

问题概述 一家钢琴专卖店,根据以往的销售经验,平均每周只能售出一架钢琴,现在经理指定的存贮策略是,每周末检查库存存量,仅当库存量为零时,才订购3架供下周销售;否则就不订购。试估计这种策略下…

Linux环境变量配置说明(配置jdk为例-摘录自尚硅谷技术文档)

配置环境变量的不同方法 Linux的环境变量可在多个文件中配置,如/etc/profile,/etc/profile.d/.sh,~/.bashrc,~/.bash_profile等,下面说明上述几个文件之间的关系和区别。 bash的运行模式可分为login shell和non-login shell。 例…

Zookeeper 集群安装(linux )

1、规划 节点名称安装软件hadoop01jdk1.8 zookeeper3.4.6hadoop02jdk1.8 zookeeper3.4.6hadoop03jdk1.8 zookeeper3.4.62、下载jdk 和 zookeeper 3、上传jdk和zookeeper 到3台机器上解压 目录:/opt/software/ tar -zxvf jdk1.8.0_191.tar.gz tar -zxvf zookeeper-3.4.6…