装饰模式是一个比较抽象的概念。即使在代码中运用了该模式,或许也不太容易意识到。
它的概念很简单,在不影响模块现有功能的前提下,为模块增添新的功能。这里的模块可以是方法、函数、类,甚至是系统等等。
JavaScript
类的装饰
JS 类支持使用装饰器,装饰器是一个函数。它接受一个 target 参数,持有待装饰类的引用。
function runDecorator(target) {
target.prototype.run = function() {
console.log(this, 'run');
};
}
@runDecorator
class Cat {
}
const cat = new Cat();
cat.run(); // 输出:"run"
上述代码实现为 Cat 类添加 run() 方法。但这么使用装饰器很鸡肋,我们完全可以直接在类中定义这个方法。
类装饰器更多的是用来, hook 类已经存在的方法,并添加新的功能,以便该新功能能够复用。
比如有一个需求,如何以日志的形式监控,类的方法被调用。粗暴的做法是,手动修改类的每一个方法,为它们加上监控代码。这显然不科学。
用装饰器可以轻松优雅的实现该需求。
function log(target) {
const p = target.prototype;
for (const key in p) {
if (!p.hasOwnProperty(key)) break;
const func = p[key];
// 过滤出 p 中的方法
if (typeof func === 'function') {
// 创建新方法
const newFunc = function() {
// 输出日志
console.log(key.toString());
// 调用原方法
func.apply(this, arguments);
}
// 更新方法
target.prototype[key] = newFunc;
}
};
}
@log
class Test { }
具体过程注释已经比较清晰了。在不改变原有模块(方法)的前提下,为模块「新增」功能,这里的新增,并没有影响原来方法的功能。
更多关于 JS 装饰器的使用,请参考 ES6 Decorator。
OC Method Swizzling
Objective-C Method Swizzling,即方法交换,是基于 runtime 实现的,它在非侵入的前提下,给方法添加新功能,很好地体现了装饰模式的思想。
// ...
Class class = [self class];
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
// ...
// 使用
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", self);
}
示例代码来源:Method Swizzling
假如一个类的同一个方法在多个 Category 中被交换,那么这个方法最终的行为会是怎么样的呢?
答案是所有的交换方法都会生效,这也是我们预期的效果。但是它存在一个很大的缺陷,如果有两个交换方法的名字不慎重名,那么这两个交换方法都不会被调用,更糟糕的是因为是在 runtime 交换的,所以编译器也不会有任何提示。
此外,类的加载与它在 Build Phases 中的位置有关,我们当然不能依赖这个位置顺序来手动决定类的加载顺序。所以如果有多次方法交换,它们的调用顺序是不确定的。
而 JS 的 Decorator 调用顺序是可以指定的。
Swift 协议和继承
Protocol 是 Swift 语言的一大特性,不管是 class, struct 还是 enum 都能使用协议进行扩展。加上 Protocol 不仅仅只是添加声明,还能「自带」 实现,更是让 Swift 如虎添翼。
在形式上,协议和继承非常的像,而且它们都是装饰模式的应用。但是从装饰模式的角度,它们存在不少区别。暂且抛开其它的区别,相对于协议,继承是一种低效的装饰。
虽然继承能够复用父类的代码,但是添加在子类中的新功能想要复用,只能够再继承,该功能被限制在了特定的类型中。如果要在其它类中使用,只能再写一遍。
如果没有特殊的约束,协议是可以被任何类型遵循的,也就提高了协议中自带实现的复用率。
继承和协议的使用形如这样:
class A: Father, Protocol {
}
A 继承自 Father,并遵循了 Protocol 协议。这里 A 继承 Father 并添加自己的实现,所以它是 Father 的装饰,而 A 遵循 Protocol,是 Protocol 对它进行装饰,两种实现的装饰主体完全不同。
如果将协议从遵循的类中移除,该类只是缺失协议提供的功能,类本身还是可以正常工作。但如果是继承,修改父类的代码,子类将会受到影响,然而子类作为「装饰」,行为会被它所装饰的主题对象的改变,这显然不科学,是一种伪装饰。
相对来说,协议是低耦合的,也更能体现装饰模式的核心思想。
总结
从更高的维度讲,装饰模式是 AOP 编程范式的运用。以一种非浸入,模块化的方式为已有的代码添加新特性。个人认为它的本质作用是便于代码的维护,插件式的可插拔。
然而选择一定会存在成本,装饰模式将功能分散到不同的地方,可能会存在功能上的重复和冲突甚至是相互抵消,增加复杂性,也增加了学习和理解的难度。