设计模式 ─── 行为拓展

行为拓展,也就是为对象添加功能。如果接触设计模式之前,我们为一个对象添加新功能也就是直接在类里面添加对应代码,继承或者写个分类(这其实就是装饰)。现在你有其他方法了(虽然我觉得责任链其实归结于对象解耦)。
对于这三种设计模式,都是在拓展对象行为的同时,对对象进行最少的修改甚至不作修改。(这个也是一个很重要的设计原则)
涉及到行为拓展的有以下三种设计模式:
① 访问者(抽离功能到访问类)
② 装饰(Object组合创建一个装饰对象)
③ 责任链

一. 访问者

在软件设计中,为了扩展类的功能而往一个类里塞进太多方法,类就会变得极为复杂。更好的做法是创建外部的类来拓展它,而对原始的代码不作太多改动。
访问者模式涉及两个关键角色:访问者和它访问的元素,元素可以是任何对象,但通常是“部分-整体”结构中的节点。如下类图所示,Visitor协议声明了两个很像的visit方法,用于访问和处理各种Element类型的ConcreteVisitor(1或2)实现了这一协议及抽象方法。visit操作定义了针对Element类型的适当操作。Client创建ConcreteVisit(1或2)的对象,并将其传给一个Element对象结构,Element对象结构中有个方法接受一般化的Visitor类型。继承Element的类中,所有的acceptVisitor:方法中的操作几乎一样,就是让Visitor对象访问发起调用的具体Element对象。实际使用的visit消息,定义在每个具体Element类中,这是具体Element类之间唯一不同点。每当把acceptVisitor:消息传给Element结构,这个消息就会被转发给每个节点。在运行时确定Element对象的实际类型,再根据实际类型决定该调用哪个visit方法。

访问者模式.png

举个例子🌰:以房屋承包商为例,Plumber(管道工)和Electrician(电工)是访问者。House是个复杂结构,包含有Fixable(可修理的)抽象物品,Contractor(承包商)可对其进行访问并维修。Plumber可以用其专有的visitPlumbing:操作访问House的Plumbing(管路)结构。同样的,电工可用他的visitElectrical:操作来访问Electrical(电路)结构。普通的Contractor好像既会修Plumbing又会修Electrical,实际上它把这些工作转包给能实际完成工作的人。所以,业主不必学习新的技能(修改现有的代码),只需要打开门让承包商进来,承包商(访问者)可以执行特定的技术性工作。

承包商和业主.png

访问者模式是拓展组合结构功能的一种强有力的方式。如果组合结构具有精心设计的基本操作,而且将来也不会变更,就可以用访问者模式,用各种不同用途的访问者,以同样的方式访问这个组合对象。访问者模式用尽可能少的修改,可以把组合结构与其他访问者类中的相关算法分离。
为什么访问者模式一直和组合绑在一起?在组合模式中,如果直接修改,就需要在组合的基接口,同时修改每个节点类。但对于访问者模式,通常就不再需要组合体类的接口了。如果使用的是范畴(分类),那我们针对不同的节点,就得创建对应的独立的分类。而使用访问者的话,只需要一个,它可以把针对所有节点的相关算法合并在移除。同样,如需再次拓展这些节点就需要修改已有的范畴或创建新的范畴(这样其实与直接修改相比差别不大)。很多情况下,使用范畴不比访问者好多少(如果是非组合情况下,使用范畴还是相当方便)。
当然,访问者模式也有个显著缺点,访问者与目标类耦合在一起。如果访问者需要支持新的类,访问者的父类和子类都要修改,才能反映新的功能(所以这边也要看是不是经常在目标类家族中添加新类)。

在以下场景,需要考虑访问者模式:

  • 一个复杂的对象结构包含很多其他对象,它们有不同的接口(比如组合体),但是想对这些对象实施一些依赖于其具体类型的操作。
  • 需要对一个组合结构中的对象进行不相关的操作,但是不想让这些操作“污染”这些对象的类。可以将相关的操作集中起来,定义在一个访问者类中,并在需要访问者中定义的操作时使用它。
  • 定义复杂结构的类很少作修改,但经常需要向其添加新的操作。

我的理解:需要增加功能时,不想破坏原有的类(比如在父类添加功能就会破坏),这功能又不应该放在原有的类中(不属于这个类的功能)。就需要抽离出访问者来实现。
比如有地图上的位置类A(城市类)和B(景点类)都继承Location(位置类),想让他们输出自己的位置信息,一种是在Location添加输出的相关代码,如上所说的,不想破坏基类,而且又觉得输出信息不应该属于原有类的功能,那就需要添加一个访问者(拥有访问A和B的功能),当然Location,以及A和B都需要添加一小部分代码acceptVisitor,A和B重写acceptVisitor,让Visitor获取自身(A或B),然后执行输出信息的功能。对于Location类型的类,只需要执行acceptVisitor,不管怎么输出(Visitor搞定),谁要输出(A和B内定义了),都没有影响。而且我们也能通过抽离Visitor协议,根据不同的输出要求生成不同的Visitor。

Visitor.jpg

二. 装饰

对比访问者模式,装饰是通过从外部进行“装饰”,就像照片外面加了相框一样。装饰模式是动态地给一个对象添加一些额外的职责,就拓展功能来说,装饰模式相比生成子类更为灵活。
标准的装饰器模式包括一个抽象Component父类,它为其他具体组件(component)声明了一些操作。抽象的Component类可被细化为另一个叫做Decorator的抽象类。Decorator包含另一个Component的引用。ConcreteDecorator为其他Component或Decorator定义了几个拓展行为,并且会在自己的操作中执行内嵌的Component操作。
Compont定义了一些抽象操作,其具体类将进行重载以实现自己特定的操作。Decorator是一个抽象类,它将一个Component(或Decorator)内嵌到Decorator对象,定义了扩展这个Component的实例的“装饰性”行为。默认的operation方法只是向内嵌的Component发送一个消息。ConcreteDecoratorA和ConcreteDecoratorB重载父类的operation,通过super把自己增加的行为拓展给Component的operation。(如果只需要向Component添加一种职责,可以省掉抽象的Decorator类)

装饰器模式.png

举个例子🌰:为UIImage创建图像滤镜。装饰模式是向对象添加新的行为与职责的一种方式,它不改变任何现有的行为与接口。所以我们定义一个跟这个图像对象一样的类,但它包含对另一个图像对象的引用,增加这个图像对象的行为。
我们使用滤镜是就可以像下面这样使用:

// 普通图片 → 看成是Component
UIImage *image = [UIImage imageNamed:@"Image.png"];
// 变形滤镜 → 看成是ConcreteDecoratorA(为Component装饰)
id <ImageComponent> transformedImage = [[ImageTransformFilter alloc] initWithImageComponent:image transform:finalTransform];
// 阴影滤镜 → ConcreteDecoratorB (这边为一个Decorator装饰,也就是ConcreteDecoratorA)
id <ImageComponent> finalImage = [[ImageShadowFilter alloc] initWithImageComponent:transformedImage];

这两种滤镜都是继承于ImageFilter(ImageFilter内部的属性和方法如下,主要还是绑定Component和转发方法)。当使用finalImage时,会将finalImage画在view上,就会调用<ImageComponent>协议的draw方法,通过转发调用finalImage自身的apply方法,从而在Image的上下文上添加阴影。

@interface ImageFilter : NSObject <ImageComponent>

@property (nonatomic, retain) id <ImageComponent> component;
// 每个滤镜自己的执行方法(当做自己的策略)
- (void) apply;
// 初始化方法(绑定一个component)
- (id) initWithImageComponent:(id <ImageComponent>) component;
// 实现draw方法转发到apply
- (id) forwardingTargetForSelector:(SEL)aSelector;
对应滤镜的类图.png

装饰器是从外部改变内嵌的Component,或者说是改变对象的外表,每个节点也不知道谁在改变它,因为变化的部分是外面的类(比如滤镜类)。还有一种设计模式叫策略模式,是通过改变对象的“内容”,已滤镜为例,就好像直接在draw方法中改变,当然会注入对应的策略,所以说每个知道一组预定义的变更方式。两种模式的区别如下:

装饰与策略的异同.png

当然我们还可以使用范畴(也就是我们说的分类)来实现装饰模式。不过,在范畴方式中,滤镜是实例方法,而真正的子类方法(我们上面实现的)中是子类。真正子类方式的实现使用一种较为结构化的方式连接各种装饰器。范畴的方式更为简单和轻量,适合现有类只需要少量装饰器的应用程序。虽然范畴不同于实际的子类化,不能严格实现模式的原始风格,但它实现了解决同样问题的意图。实际的子类化虽然实现起来比较复杂,却更容易理解,图像的任何组合都能当做对象,我们可以动态应用或删除,而不影响UIImage原有行为的完整性。

在以下情况下,应该考虑使用装饰模式:

  • 想要在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
  • 想要拓展一个类的行为却做不到。类定义可能被隐藏,无法进行子类化;或者对类的每个行为的拓展,为支持每种功能组合,将产生大量的子类。
  • 对类的职责的拓展是可选的。

我的理解:需要增加功能时,不想破坏原有的类,在封装一层装饰。装饰类包含原功能类(组合),增加的功能是在原功能类的基础上实现的,所以可以在原功能上添加某些功能。最简单的结构如下。当然,我们需要定义多个装饰器时,可以再继承重写对应的operation

class Decorator: Component {

    private var component: Component

    init(_ component: Component) {
        self.component = component
    }

    /// The Decorator delegates all work to the wrapped component.
    func operation() -> String {
        return component.operation()
    }
}

三. 责任链

责任链模式与装饰模式在结构上有点类似,但是它实现不同的目的。
责任链模式使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间发生耦合。此模式将这些对象连成一条链,并沿着这条链传递请求,知道有一个对象处理它位置。(通俗讲就是,“这个问题我不懂,也许你懂”,或者,“我的活干完了,现在该你了”)。
责任链中,对象引用了同一类型的另一个对象,形成一条链。链中每个对象实现了相同的方法,处理链中第一个对象发起的同一请求,如果第一个对象不知道如何处理请求,它就把请求传给下一个响应者(Successor)。
Handler 是上层抽象类,定义了处理请求handleRequest的方法。ConcreteHandler1和ConcreteHandler2实现了handleRequest方法,来处理它们认识的请求对象。Handler还有个Successor的实例(即下一个响应者)。消息从第一个Handler实例开始传递,如果这个实例不知道,就传给它的Successor(也是一个Handler实例),直到请求被传到链中的最后一个Handler。

责任链.png

举个例子🌰:以RPG游戏人物防御为例,防御道具可以是盾牌或铠甲,每种形式的防御只能应付一种特定的攻击。如果防御道具无法防御某种攻击,就会传给下一个道具,直到攻击到人物。
在游戏过程中,任何攻击处理程序都能在任何时间被添加或删除,而不会影响人物的其他行为(解耦的好处)。对此类设计,责任链模式是很自然的选择。否则,攻击处理程序的复杂组合会让人物的代码非常庞大,让处理程序的变更非常困难。

游戏中的责任链.png

之前说过,责任链模式在这一模块是行为拓展,它让链头的成员拥有了链的所有处理能力。同时消除耦合也是一个主要的功能,它让我们不用考虑要处理的请求是什么,要处理该请求的对象是哪个。如果不用责任链模式,得对这些一一去判断,让对象一一去对应请求。

在以下情况,需要考虑责任链模式:

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

推荐阅读更多精彩内容