我理解的装饰模式

429 阅读4分钟

装饰模式是一个比较抽象的概念。即使在代码中运用了该模式,或许也不太容易意识到。

它的概念很简单,在不影响模块现有功能的前提下,为模块增添新的功能。这里的模块可以是方法、函数、类,甚至是系统等等。

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 编程范式的运用。以一种非浸入,模块化的方式为已有的代码添加新特性。个人认为它的本质作用是便于代码的维护,插件式的可插拔。

然而选择一定会存在成本,装饰模式将功能分散到不同的地方,可能会存在功能上的重复和冲突甚至是相互抵消,增加复杂性,也增加了学习和理解的难度。