设计模式之装饰器模式

145 阅读6分钟

做音视频相关的工作经常会碰到各种各样的码流,如H264、H265、AAC、PCM,……从抽象的角度来看,它们都是流stream,因此可以从一个基类的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(){}
};

class H264Stream: public Stream{
public:
    virtual char Read(int number){
        //读文件流
    }
    virtual void Seek(int position){
        //定位文件流
    }
    virtual void Write(char data){
        //写文件流
    }
};

class H265Stream: public Stream{
public:
    virtual char Read(int number){
        //读文件流
    }
    virtual void Seek(int position){
        //定位文件流
    }
    virtual void Write(char data){
        //写文件流
    }
};

class AACStream: public Stream{
public:
    virtual char Read(int number){
        //读文件流
    }
    virtual void Seek(int position){
        //定位文件流
    }
    virtual void Write(char data){
        //写文件流
    }
};

在继承得到各种具体的流类型后,还需要对流进行各种操作。最简单的有封装,比如RTP封装、PS封装、TS封装,……以这三种为例,分别对上面的H264、H265和AAC进行操作。比较直白的就是分别对每一种流再继承出三个封装类,以用RTP对H264进行封装为例。

class RtpH264Stream :public H264Stream{
public:
    virtual char Read(int number){       
        //额外的封装操作...
        H264Stream::Read(number);//读文件流
		//额外的封装操作
    }
    virtual void Seek(int position){
        //额外的封装操作...
        H264Stream::Seek(position);//定位文件流
        //额外的封装操作...
    }
    virtual void Write(byte data){
        //额外的封装操作...
        H264Stream::Write(data);//写文件流
        //额外的封装操作...
    }
};

当然,其他封装操作以及对其他流的封装操作也类似。这样,三个流类型又多出9个封装操作类。至此,总的类个数达到了1+3+9=13个。 

仔细观察上面的类代码,其实很容易发现他们有相当的一部分代码是类似重复的,因此我们可以思考利用多态特性来进行优化。首先,我们可以看到,不管是主体类还是封装类,他们中的read、write和seek操作其实都是来自于抽象类stream,不过是在具体的主体类中对他们进行了实现,这一点也是利用多态性进行优化的基础。 

通过观察RtpH264Stream类可以发现,其与其他封装类最大的不同有:继承的基类不一样,它继承自H264Stream,而其他封装类比如RtpH265Stream继承自H265Stream;调用的seek、write和read方法前有域作用域符。而正如上面所说的,这些不同都源自于同一个抽象基类stream,所以可以进行优化如下。

class RtpStream: public Stream{    
    Stream* stream; //...
public:
    RtpStream(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);	//写文件流
        //额外的封装操作...
    }
};

可见,让RTP封装类继承自stream,再引入一个stream的多态指针成员变量,就能将对三个音视频流(H264、H265、AAC)的RTP封装类优化成一个,大大降低了重复代码量。使用时,只要将H264或者H265或者AAC的对象指针传递给RtpStream的构造函数,在调用read、write和seek时就能分别调用各自的实现函数了。

    H264Stream* s1=new H264Stream();
    RtpStream* s2=new RtpStream(s1);
	s2.read();  // 最终调用的是s1中的read

至此,代码已经优化了最关键的一步,但是仔细观察可以发现,三个封装类RtpStream,PsStream,TsStream中都存在stream成员变量,而他们又都同时来自于抽象基类,因此可以进一步优化提升。我们可以用一个装饰类来表示,如下:

DecoratorStream: public Stream{
protected:
	// 由于多个子类有相同的成员Stream*,所以这个成员往上提
    Stream* stream;//...    
    DecoratorStream(Stream *stm):stream(stm){    
    }    
};

class RtpStream: public DecoratorStream{
public:
    RtpStream(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);	//写文件流
        //额外的封装操作...
    }
};

装饰类DecoratorStream继承自基类同时又有一个基类类型的指针,这种特点使得该类在绝大部分情况下所起的作用就是一个装饰类。此时,三个封装类中仅保留了各自独有的操作代码,不同的部分分别由DecoratorStream和Stream两个基类来承担,做到了代码的优化。下面是完整地伪代码。

class Stream{
public:
    virtual char Read(int number)=0;
    virtual void Seek(int position)=0;
    virtual void Write(char data)=0;    
    virtual ~Stream(){}
};

class H264Stream: public Stream{
public:
    virtual char Read(int number){
        //读文件流
    }
    virtual void Seek(int position){
        //定位文件流
    }
    virtual void Write(char data){
        //写文件流
    }
};

class H265Stream: public Stream{
public:
    virtual char Read(int number){
        //读文件流
    }
    virtual void Seek(int position){
        //定位文件流
    }
    virtual void Write(char data){
        //写文件流
    }
};

class AACStream: public Stream{
public:
    virtual char Read(int number){
        //读文件流
    }
    virtual void Seek(int position){
        //定位文件流
    }
    virtual void Write(char data){
        //写文件流
    }
};

DecoratorStream: public Stream{
protected:
	// 由于多个子类有相同的成员Stream*,所以这个成员提到装饰类中
    Stream* stream;//...    
    DecoratorStream(Stream *stm):stream(stm){    
    }    
};

class RtpStream: public DecoratorStream{
public:
    RtpStream(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 PsStream: public DecoratorStream{
public:
    PsStream(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 TsStream: public DecoratorStream{
public:
    TsStream(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);	//写文件流
        //额外的封装操作...
    }
};

int main(){
    H264Stream* s1=new H264Stream();
    RtpStream* s2=new RtpStream(s1);
	PsStream* s3=new PsStream(s1);
	TsStream* s4=new TsStream(s1);
	s2.read();
	s3.read();
	s4.read();
}

总结:

装饰器模式适用于子类急剧膨胀的业务场景中,主要是做到明确职责,责任单一。其实仔细品上面的分析过程,从基类Stream派生出H264流、H265流和AAC流是很自然的,后三者都是前者的自然延伸。但是从H264流或者H265流或者AAC流派生到RtpH264Stream之类的子类其实并不恰当,前面是流本身的衍生,这里却是对流的处理,所以更适合组合的方式。在类中加入Stream* stream就是采用了一种组合的设计方式。