行为拓展,也就是为对象添加功能。如果接触设计模式之前,我们为一个对象添加新功能也就是直接在类里面添加对应代码,继承或者写个分类(这其实就是装饰)。现在你有其他方法了(虽然我觉得责任链其实归结于对象解耦)。
对于这三种设计模式,都是在拓展对象行为的同时,对对象进行最少的修改甚至不作修改。(这个也是一个很重要的设计原则)
涉及到行为拓展的有以下三种设计模式:
① 访问者(抽离功能到访问类)
② 装饰(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方法。
举个例子🌰:以房屋承包商为例,Plumber(管道工)和Electrician(电工)是访问者。House是个复杂结构,包含有Fixable(可修理的)抽象物品,Contractor(承包商)可对其进行访问并维修。Plumber可以用其专有的visitPlumbing:
操作访问House的Plumbing(管路)结构。同样的,电工可用他的visitElectrical:
操作来访问Electrical(电路)结构。普通的Contractor好像既会修Plumbing又会修Electrical,实际上它把这些工作转包给能实际完成工作的人。所以,业主不必学习新的技能(修改现有的代码),只需要打开门让承包商进来,承包商(访问者)可以执行特定的技术性工作。
访问者模式是拓展组合结构功能的一种强有力的方式。如果组合结构具有精心设计的基本操作,而且将来也不会变更,就可以用访问者模式,用各种不同用途的访问者,以同样的方式访问这个组合对象。访问者模式用尽可能少的修改,可以把组合结构与其他访问者类中的相关算法分离。
为什么访问者模式一直和组合绑在一起?在组合模式中,如果直接修改,就需要在组合的基接口,同时修改每个节点类。但对于访问者模式,通常就不再需要组合体类的接口了。如果使用的是范畴(分类),那我们针对不同的节点,就得创建对应的独立的分类。而使用访问者的话,只需要一个,它可以把针对所有节点的相关算法合并在移除。同样,如需再次拓展这些节点就需要修改已有的范畴或创建新的范畴(这样其实与直接修改相比差别不大)。很多情况下,使用范畴不比访问者好多少(如果是非组合情况下,使用范畴还是相当方便)。
当然,访问者模式也有个显著缺点,访问者与目标类耦合在一起。如果访问者需要支持新的类,访问者的父类和子类都要修改,才能反映新的功能(所以这边也要看是不是经常在目标类家族中添加新类)。
在以下场景,需要考虑访问者模式:
- 一个复杂的对象结构包含很多其他对象,它们有不同的接口(比如组合体),但是想对这些对象实施一些依赖于其具体类型的操作。
- 需要对一个组合结构中的对象进行不相关的操作,但是不想让这些操作“污染”这些对象的类。可以将相关的操作集中起来,定义在一个访问者类中,并在需要访问者中定义的操作时使用它。
- 定义复杂结构的类很少作修改,但经常需要向其添加新的操作。
我的理解:需要增加功能时,不想破坏原有的类(比如在父类添加功能就会破坏),这功能又不应该放在原有的类中(不属于这个类的功能)。就需要抽离出访问者来实现。
比如有地图上的位置类A(城市类)和B(景点类)都继承Location(位置类),想让他们输出自己的位置信息,一种是在Location添加输出的相关代码,如上所说的,不想破坏基类,而且又觉得输出信息不应该属于原有类的功能,那就需要添加一个访问者(拥有访问A和B的功能),当然Location,以及A和B都需要添加一小部分代码acceptVisitor,A和B重写acceptVisitor,让Visitor获取自身(A或B),然后执行输出信息的功能。对于Location类型的类,只需要执行acceptVisitor,不管怎么输出(Visitor搞定),谁要输出(A和B内定义了),都没有影响。而且我们也能通过抽离Visitor协议,根据不同的输出要求生成不同的Visitor。
二. 装饰
对比访问者模式,装饰是通过从外部进行“装饰”,就像照片外面加了相框一样。装饰模式是动态地给一个对象添加一些额外的职责,就拓展功能来说,装饰模式相比生成子类更为灵活。
标准的装饰器模式包括一个抽象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类)
举个例子🌰:为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;
装饰器是从外部改变内嵌的Component,或者说是改变对象的外表,每个节点也不知道谁在改变它,因为变化的部分是外面的类(比如滤镜类)。还有一种设计模式叫策略模式,是通过改变对象的“内容”,已滤镜为例,就好像直接在draw
方法中改变,当然会注入对应的策略,所以说每个知道一组预定义的变更方式。两种模式的区别如下:
当然我们还可以使用范畴(也就是我们说的分类)来实现装饰模式。不过,在范畴方式中,滤镜是实例方法,而真正的子类方法(我们上面实现的)中是子类。真正子类方式的实现使用一种较为结构化的方式连接各种装饰器。范畴的方式更为简单和轻量,适合现有类只需要少量装饰器的应用程序。虽然范畴不同于实际的子类化,不能严格实现模式的原始风格,但它实现了解决同样问题的意图。实际的子类化虽然实现起来比较复杂,却更容易理解,图像的任何组合都能当做对象,我们可以动态应用或删除,而不影响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。
举个例子🌰:以RPG游戏人物防御为例,防御道具可以是盾牌或铠甲,每种形式的防御只能应付一种特定的攻击。如果防御道具无法防御某种攻击,就会传给下一个道具,直到攻击到人物。
在游戏过程中,任何攻击处理程序都能在任何时间被添加或删除,而不会影响人物的其他行为(解耦的好处)。对此类设计,责任链模式是很自然的选择。否则,攻击处理程序的复杂组合会让人物的代码非常庞大,让处理程序的变更非常困难。
之前说过,责任链模式在这一模块是行为拓展,它让链头的成员拥有了链的所有处理能力。同时消除耦合也是一个主要的功能,它让我们不用考虑要处理的请求是什么,要处理该请求的对象是哪个。如果不用责任链模式,得对这些一一去判断,让对象一一去对应请求。
在以下情况,需要考虑责任链模式:
- 有多个对象可以处理请求,而处理程序只有在运行时才能确定。
- 向一组对象发出请求,而不想显式指定处理请求的特定处理程序。