一、“单一职责”模式
概述:在软件组件的设计中,如果责任划分的不清晰,使用继承得到的结果往往是随着需求的变化,子类急剧膨胀,同时充斥着重复代码,这时候的关键是划清责任.
这个思想的典型模式是:Decorator装饰模式、Bridge桥模式.
ps:这并非表示其他模式就不注重责任的问题,只是这两个模式在这个特性上尤为突出。
二、模式场景
这是一段关于流文件处理的代码思路
//业务操作
protocol Steam{
func Read()
func Seek()
func Write()
}
//主体类
class FileStream:Steam{
func Read() {} //读文件流
func Seek() {} //定位文件流
func Write() {} //写文件流
}
class NetworkStream:Steam{
func Read() { } //读网络流
func Seek() { } //定位网络流
func Write() {} //写网络流
}
class MemoryStream:Steam{
...
//内容类似
}
如上述代码所示,我们根据一个抽象协议,来创建了三种流,这样做很好,将流这个概念单独抽象出来。
那么下一个阶段我们接着思考如果,我们要再设计一个加密了的文件流,那似乎很简单,我们只需要使用继承,像这样写:
//扩展操作
class CryptoFileStream:FileStream{//加密的文件流,使用继承来操作
override func Read() {
//...省略额外的加密操作
super.Read()
}
override func Seek() {
//...省略额外的加密操作
super.Seek()
}
override func Write() {
//...省略额外的加密操作
super.Write()
}
}
这样的设计似乎很容易又很好,假如再有一些加密网络流和内存流的需求,So easy,照着路子做!
class CryptoNetworkStream:NetworkStream{//加密的内存流
override func Read() {
//...省略额外的加密操作
super.Read()
}
override func Seek() {
//...省略额外的加密操作
super.Seek()
}
override func Write() {
//...省略额外的加密操作
super.Write()
}
}
class CryptoMemoryStream:MemoryStream{//加密的内存流
//略,如上类似
}
这个时候依然很好,似乎没有构成设计上的问题,那么再来一个需求,现在我们想在操作上需要一种拥有缓存操作的流了.假如你这个时候,看到这个需求,还没有意识到设计上的问题,那么你就可以阅读全文了.因为你肯定会如下设计:
class BufferedFileStream:FileStream{
//...
}
class BufferedNetworkStream:NetworkStream{
//...
}
class BufferedMemoryStream:MemoryStream{
//...
}
还没完呢,这个时候自然而然,肯定会延伸出既加密又缓存的流的设计出现.那必然也会是这样:
class CryptoBufferedFileStream:FileStream{
override func Read() {
//额外的加密操作
//额外的缓冲操作
super.Read()
}
override func Seek() {
//额外的加密操作
//额外的缓冲操作
super.Seek()
}
override func Write() {
//额外的加密操作
//额外的缓冲操作
super.Write()
}
}
class CryptoBufferedNetworkStream:NetworkStream{
//略
}
class CryptoBufferedMemoryStream:MemoryStream{
//略
}
这个时候你肯定闻到了一股坏坏的味道,因为这个设计似乎看上去漏洞百出,但是很多情况下,我们如果直接顺着需求,很容易演变成这样,而且这有时候意识到时,这个代码规模已经不小了.来看看这个设计的结构图:
假如在这个设计下,再加入类型流的设计,那得设计更多个类.这显然会造成代码的冗余.
三、尝试改良
我们可以从组合优于继承的角度,将上述的加密文件流,这样改写:
class CryptoFileStream{//加密操作
var stream:FileStream?
func Read() {
//额外的加密操作
stream?.Read()
}
func Seek() {
//额外的加密操作
stream?.Seek()
}
func Write() {
//额外的加密操作
stream?.Write()
}
}
这样我们通过组合,在功能上,也和继承没有区别.然后我们再接着改写后面的内存流、网络流的加密后不难发现,这三段代码只有成员stream的类型不一样,其他都几乎一样了.于是乎,利用多态,我们将子类(FileStream)换成接口(Stream),将编译时绑定换成运行时绑定.这样的考虑,就取消了FileStream和NetworkStream这样类型的差异.
class CryptoStream{//加密操作,通过运行时绑定类型,替代编译时绑定,也就取消诸如CryptoFileStream的设计
var stream:Stream?
func Read() {
//额外的加密操作
stream?.Read()
}
func Seek() {
//额外的加密操作
stream?.Seek()
}
func Write() {
//额外的加密操作
stream?.Write()
}
}
但还有一个问题,我们把Read,Seek,Write这三个方法的规范同时也取消了,于是我们接下来,进行如下微改
class CryptoStream:Stream{//继承的接口Stream,现在的目的变为规范接口
var stream:Stream?
init( st : Stream ){
stream = st
}
func Read() {
//额外的加密操作
stream?.Read()
}
func Seek() {
//额外的加密操作
stream?.Seek()
}
func Write() {
//额外的加密操作
stream?.Write()
}
}
这中间已经发生了非常大的变化,因为我们既规定了Stream的接口,又设置了stream作为多态成员.这是一个非常有意思的变化.并且我们只要这样来使用就能达到效果:
var s1 = FileStream()
var e1 = CryptoStream(st: s1)
var e2 = BufferedStream(st: e1)
而且这样做,通过运行时绑定,我只要像上面e1和e2的组合,就达到了原来结合版的效果.
到目前为止,我们的设计已经发生了极大的改变,现在结构图大致是这样:
三、完善装饰模式
这个时候我们其实代码还是要重复的部分,如 CryptoStream和BufferedStream还是会有重复的stream字段,如下面代码所示:
class CryptoStream:Stream{//加密操作
var stream:Stream?
init( st : Stream ){
stream = st
}
...//省略Read.Seek.Write方法
}
class BufferedStream:Stream{
var stream:Stream?
init( st : Stream ){
stream = st
}
...//省略Read.Seek.Write方法
}
那现在该怎么做,我们即使在抽象的Stream接口放入stream字段,还是要写出来,因为它不是类.而且就是把protocol换成class,FileSteam这样的类型类不需要这个stream多态字段.所以我们开始设计一个中间的基类
class DecoratorStream:Stream{
var stream:Stream?
init( st : Stream ){
stream = st
}
func Read() {}
func Seek() {}
func Write() {}
}
这样我们的扩展类只需要继承这个中间类,就能达到目前的最佳效果了,如下所示:
class CryptoStream:DecoratorStream{//加密操作
override func Read() {
//额外的加密操作
stream?.Read()
}
override func Seek() {
//额外的加密操作
stream?.Seek()
}
override func Write() {
//额外的加密操作
stream?.Write()
}
}
现在改造算是完成了.我们从原来的1+n+n*m/2(n为类型类的数量,m为扩展类的数量)变成1+n+1+m。
这次的场景其实是源于对继承的不良使用,疯狂的使用继承.其实很多操作并不需要我们使用继承,静下心来分析,就能发现通过对象组合就能解决问题
全篇最妙的操作是指针和接口的如下使用:
class CryptoStream:Stream{//继承的接口Stream,现在的目的变为规范接口
var stream:Stream?
...//省略操作
}
这是整个设计的核心,用指针来支持未来多态的变化.这就是装饰模式的妙处。Stream接口就像一层装饰一样,因此称之为装饰模式.
四、模式总结
下面是GOF对这个模式的评价:
动态地给一个对象增加一些额外的职责.就增加功能而言,Decorator模式比生成子类(继承)更为灵活(消除重复代码&减少子类个数)
要点总结如下:
1.通过组合而非继承的手法,Decorator模式实现了在运行时动态扩展对象功能的能力,而且根据需要扩展多个功能.而没有使用继承带来的"灵活性差"和"多子类衍生的问题"。
2.Decorator类在接口上表现为is - a的继承关系,即Decorator继承了Component类所具有的接口,但在实现上,表现了has a的关系.又使用了一个Component类。
3.Decorator模式的目的并非解决"多子类衍生的多继承"问题,Decorator模式应用要点在于解决"主体类多个方向上的扩展".
(主体操作和扩展操作应该像第二个结构,分开分支继承)