代码也写了几年了,设计模式处于看了忘,忘了看的状态,最近对设计模式有了点感觉,索性就再学习总结下吧。
大部分讲设计模式的文章都是使用的 Java、C++ 这样的以类为基础的静态类型语言,作为前端开发者,js 这门基于原型的动态语言,函数成为了一等公民,在实现一些设计模式上稍显不同,甚至简单到不像使用了设计模式,有时候也会产生些困惑。
下面按照「场景」-「设计模式定义」- 「代码实现」- 「易混设计模式」-「总」的顺序来总结一下,如有不当之处,欢迎交流讨论。
场景
微信小程序定义一个页面是通过微信提供的Page 方法,然后传入一个配置对象进去。
Page({
data: { // 参与页面渲染的数据
logs: []
},
onLoad: function () {
// 页面渲染后 执行
}
})
如果我们有个需求是在每个页面加载的时候上报一些自定义数据。
最直接的当然是去每个页面加就好了,但上报数据的逻辑是一致的,一个一个加有些傻了,这里就可以用到装饰器模式了。
装饰器模式
看下维基百科的定义。
装饰器(修饰)模式,是面向对象程式领域中,一种动态地往一个类别中添加新的行为的设计模式。就功能而言,修饰模式相比生成子类别更为灵活,这样可以给某个对象而不是整个类别添加一些功能。
看一下 UML 类图和次序图。
当访问 Component1 中的operation 方法时,会先调用预先定义的两个装饰器 Decorator1 和 Decorator2 中的 operation 方法,执行一些额外操作,最后再执行原始的 operation 方法。
举一个简单的例子:
买奶茶的话可以额外加珍珠、椰果等,不同小料有不同的价格、也可以自由组合,此时就可以用到装饰器模式,对原始奶茶进行加料、算价。
原始的奶茶有一个接口和类。
interface MilkTea {
public double getCost(); // 返回奶茶的价格
public String getIngredients(); // 返回奶茶的原料
}
class SimpleMilkTea implements MilkTea {
@Override
public double getCost() {
return 10;
}
@Override
public String getIngredients() {
return "MilkTea";
}
}
下边引入装饰器,进行加料。
// 添加一个装饰器的抽象类
abstract class MilkTeaDecorator implements MilkTea {
private final MilkTea decoratedMilkTea;
public MilkTeaDecorator(MilkTea c) {
this.decoratedMilkTea = c;
}
@Override
public double getCost() {
return decoratedMilkTea.getCost();
}
@Override
public String getIngredients() {
return decoratedMilkTea.getIngredients();
}
}
// 添加珍珠
class WithPearl extends MilkTeaDecorator {
public WithPearl(MilkTea c) {
super(c); // 调用父类构造函数
}
@Override
public double getCost() {
// 调用父类方法
return super.getCost() + 2;
}
@Override
public String getIngredients() {
// 调用父类方法
return super.getIngredients() + ", 加珍珠";
}
}
// 添加椰果
class WithCoconut extends MilkTeaDecorator {
public WithCoconut(MilkTea c) {
super(c);
}
@Override
public double getCost() {
return super.getCost() + 1;
}
@Override
public String getIngredients() {
return super.getIngredients() + ", 加椰果";
}
}
让我们测试一下,
public class Main {
public static void printInfo(MilkTea c) {
System.out.println("价格: " + c.getCost() + "; 加料: " + c.getIngredients());
}
public static void main(String[] args) {
MilkTea c = new SimpleMilkTea();
printInfo(c); // 价格: 10.0; 加料: MilkTea
c = new WithPearl(new SimpleMilkTea());
printInfo(c); // 价格: 12.0; 加料: MilkTea, 加珍珠
c = new WithCoconut(new WithPearl(new SimpleMilkTea()));
printInfo(c); // 价格: 13.0; 加料: MilkTea, 加珍珠, 加椰果
}
}
未来如果需要新增一种小料,只需要新写一个装饰器类,并且可以和之前的小料随意搭配。
// 添加冰淇淋
class WithCream extends MilkTeaDecorator {
public WithCream(MilkTea c) {
super(c);
}
@Override
public double getCost() {
return super.getCost() + 5;
}
@Override
public String getIngredients() {
return super.getIngredients() + ", 加冰淇淋";
}
}
public class Main {
public static void printInfo(MilkTea c) {
System.out.println("价格: " + c.getCost() + "; 加料: " + c.getIngredients());
}
public static void main(String[] args) {
c = new WithCoconut(new WithCream(new WithPearl(new SimpleMilkTea())));
printInfo(c); // 价格: 18.0; 加料: MilkTea, 加珍珠, 加冰淇淋, 加椰果
}
}
让我们用 js 改写一下,达到同样的效果。
const SimpleMilkTea = () => {
return {
getCost() {
return 10;
},
getIngredients() {
return "MilkTea";
},
};
};
// 加珍珠
const WithPearl = (milkTea) => {
return {
getCost() {
return milkTea.getCost() + 2;
},
getIngredients() {
return milkTea.getIngredients() + ", 加珍珠";
},
};
};
// 加椰果
const WithCoconut = (milkTea) => {
return {
getCost() {
return milkTea.getCost() + 1;
},
getIngredients() {
return milkTea.getIngredients() + ", 加椰果";
},
};
};
// 加冰淇淋
const WithCream = (milkTea) => {
return {
getCost() {
return milkTea.getCost() + 5;
},
getIngredients() {
return milkTea.getIngredients() + ", 加冰淇淋";
},
};
};
// test
const printInfo = (c) => {
console.log(
"价格: " + c.getCost() + "; 加料: " + c.getIngredients()
);
};
let c = SimpleMilkTea();
printInfo(c); // 价格: 10; 加料: MilkTea
c = WithPearl(SimpleMilkTea());
printInfo(c); // 价格: 12; 加料: MilkTea, 加珍珠
c = WithCoconut(WithPearl(SimpleMilkTea()));
printInfo(c); // 价格: 13; 加料: MilkTea, 加珍珠, 加椰果
c = WithCoconut(WithCream(WithPearl(SimpleMilkTea())));
printInfo(c); // 价格: 18; 加料: MilkTea, 加珍珠, 加冰淇淋, 加椰果
没有再定义类和接口,js 中用函数直接表示。
原始的 SimpleMilkTea 方法返回一个奶茶对象,然后又定义了三个装饰函数,传入一个奶茶对象,返回一个装饰后的对象。
代码实现
回到文章最开头的场景,我们需要为每个页面加载的时候上报一些自定义数据。其实我们只需要引入一个装饰函数,将传入的 option 进行装饰返回即可。
const Base = (option) => {
const { onLoad ...rest } = option;
return {
...rest,
// 重写 onLoad 方法
onLoad(...args) {
// 增加路由字段
this.reportData(); // 上报数据
// onLoad
if (typeof onLoad === 'function') {
onLoad.call(this, ...args);
}
}
reportData() {
// 做一些事情
}
}
然后回到原始页面增加 Base 的调用即可。
Page(Base({
data: { // 参与页面渲染的数据
logs: []
},
onLoad: function () {
// 页面渲染后 执行
}
})
同理,利用装饰器模式我们也可以对其它生命周期统一插入我们需要做的事情,而不需要业务方自己再写一遍。
在大团队的话,每个业务方可能都需要在小程序生命周期做一些事情,此时只需要利用装饰器模式,编写一个装饰函数,然后在业务代码中调用即可。
最终的业务代码可能会装饰很多层,最终才传给小程序 Page 函数。
Page(Base(Log(Ce({
data: { // 参与页面渲染的数据
logs: []
},
onLoad: function () {
// 页面渲染后 执行
}
})
易混设计模式
如果之前看过代理模式,到这里可能会有一些困惑,因为和代理模式的作用很像,都是对原有对象进行包装,增强原有对象。
但还是有很大的不同点:
代理模式中,我们是直接将原对象封装到代理对象之中,对于业务方并不关系原始对象,直接使用代理对象即可。
装饰器模式中,我们只提供了装饰函数,输入原始对象,输出增强对象。输出的增强对象,还可以接着传入到新的装饰器函数中继续增强。对于业务方,可以随意组合装饰函数,但得有一个最最开始的原始对象。
再具体点:
代理模式的话,对象之间的依赖关系已经写死了,原始对象 A,新增代理对象A1, A1 的基础上再新增代理对象A2。如果我们不想要 A1 新增的功能了,我们并不能直接使用 A2 ,因为A2 已经包含了 A1 的功能,我们只能在 A 的基础上再新写一个代理对象A3。
而装饰器模式,我们只提供装饰函数A1,装饰函数 A2,然后对原始对象进行装饰 A2(A1(A))。如果不想要 A1新增的功能,只需要把 A1 这个装饰器去掉,调用 A2(A) 即可。
所以使用代理模式还是使用装饰器模式,取决于我们是要把所有功能包装后最终产出一个对象给业务方使用,还是提供许多功能,让业务方自由组合。
总
装饰器模式同样践行了「单一职责原则」,可以把对象/函数的各个功能独立出来,降低它们之间的耦合性。
业务开发中,如果某个对象/函数拥有了太多功能,可以考虑使用装饰器模式进行拆分。