用Swift整理GOF设计模式(四)--装饰模式

一、“单一职责”模式

概述:在软件组件的设计中,如果责任划分的不清晰,使用继承得到的结果往往是随着需求的变化,子类急剧膨胀,同时充斥着重复代码,这时候的关键是划清责任.

这个思想的典型模式是: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{
       //略
}

这个时候你肯定闻到了一股坏坏的味道,因为这个设计似乎看上去漏洞百出,但是很多情况下,我们如果直接顺着需求,很容易演变成这样,而且这有时候意识到时,这个代码规模已经不小了.来看看这个设计的结构图:

设计.jpeg

假如在这个设计下,再加入类型流的设计,那得设计更多个类.这显然会造成代码的冗余.

三、尝试改良

我们可以从组合优于继承的角度,将上述的加密文件流,这样改写:

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的组合,就达到了原来结合版的效果.
到目前为止,我们的设计已经发生了极大的改变,现在结构图大致是这样:

设计2

三、完善装饰模式

这个时候我们其实代码还是要重复的部分,如 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模式应用要点在于解决"主体类多个方向上的扩展".
(主体操作和扩展操作应该像第二个结构,分开分支继承)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,099评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,293评论 18 399
  • 1 场景问题# 1.1 复杂的奖金计算## 考虑这样一个实际应用:就是如何实现灵活的奖金计算。 奖金计算是相对复杂...
    七寸知架构阅读 3,798评论 4 66
  • 设计模式汇总 一、基础知识 1. 设计模式概述 定义:设计模式(Design Pattern)是一套被反复使用、多...
    MinoyJet阅读 3,820评论 1 15
  • 生命中总会遇到一些人虽然只是一面之缘,却让人深深铭记。 曾经生病住过很长一段时间医院,因为病情复杂...
    镜中的我阅读 158评论 2 2