做音视频相关的工作经常会碰到各种各样的码流,如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就是采用了一种组合的设计方式。