什么是设计模式
模式语言最早是为了解决建筑领域的复杂工程而创立的,逐渐在软件工程领域发展壮大。开山鼻祖 Christopher Alexander 是一名建筑大师。Alexander 等人创造并以实践验证了“模式语言”,描述人类建筑上的成果。Christopher Alexander 把模式定义为“在某一个情景下的问题解决方案”。
一个模式的说明应该包括四项:
- 模式名称:一个方便交流和思考的名称。实上找到恰当的名称也是设计的难点之一。
- 问题:设计模式的使用场景。
- 解决方案:描述设计模式的组成,以及各部分的职责与协作关系。
- 效果:应用设计模式能达到的效果以及各方面因素的权衡关系(如时间空间效率,可拓展性)。
软件领域与建筑领域十分相似,软件中时常有不断重复出现,可以用某种相同方式解决的问题。1994 年,GOF(Gamma、Helm、Johnson、Vlissides)共同写作了《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software)。书中收录了 23 个设计模式,将设计模式的思想应用于软件设计,这些模式的基础上提出了面向对象策略及方法的原则(如,“开闭原则”,“依赖倒置原则”……)。
该书是软件工程领域非常经典的书籍(《人月神话》也非常经典)。时至今日,书中的许多观点都还在软件工程领域发光发热。我们在设计软件时理应“首先识别出模式然后在模式的基础上创建特定的解决方案”。
设计模式与前端
前端真的是发展的非常快的领域,许多经验都还没来得及总结就在前端领域过时了,但我们还是可以在这些快速发展中窥见部分在《设计模式》提到的共性,这也是该书经典的地方。如代理、装饰器、迭代器、适配器等等耳熟能详的名词都是为了解决某一问题的设计模式。以下简单介绍我在泛前端领域观察到的两个设计模式。
代理
代理(Proxy)模式是说“给某一个对象提供一个代理,并由代理对象控制对原对象的引用”。在代理模式中存在三个角色:
- 真实实体,即被代理的对象。
- 代理者,即控制被代理对象访问的对象。
- 二者的共同接口。接口代表一种职责,只有代理者和真实实体都实现了相同的接口才能实现代理。
代理的好处在于引入了一层透明的间接,降低了使用者和真实实体之间的耦合,同时控制使用者对真实实体的访问权限。
大家都很熟悉,JavaScript 内置了 Proxy 对象。如下的代码 p 就是 target 的代理,代理逻辑就在 handler 中。
const p = new Proxy(target, handler)
比如,如下 MDN 中的代码,在获取对象属性值时,如果 prop 存在就返回真实对象的对应的值,否则就返回 37。
const handler = {
get: function (obj, prop) {
return prop in obj ? obj[prop] : 37;
},
};
这一切是对使用者透明的,也就是说使用者觉得使用的就是真实对象而察觉不到代理。后期修改逻辑也只需要修改 handler,而无需修改使用处。
我们完全可以 handler 中加入更多逻辑。比如 Vue3 中就是使用 Proxy 代理响应式数据,当监听到某个属性值改变,就可以执行相应的副作用函数更新视图。如下代码,ref(0) 就是创建了一个代理,当修改代理对象 count 的 value 属性,使用 count 的视图就会更新。
const count = ref(0);
count.value++;
该模式性能上做了一些平衡,毕竟访问对象时还需要通过一个中间层,性能上是会有相应程度的降低的。
观察者
观察者(Observer)模式是指“建立了一种目标与观察者的关系”。这样说你可能明白,如果说该模式的别名你就清楚了,观察者模式也称作“发布订阅”模式、“模型视图”模式等等。
这个模式在前端非常常见,就简单的就是我们可以监听各种事件。例如,监听某个元素的点击事件,事件发生之后自动运行 handler。
element.addEventListener("click", handler);
我们还可以自定义事件 CustomEvent。自定义事件的一个用途是使得我们的代码可以适配各种框架。前端框架的信息流存在一些差异,但使用自定义事件,可以使得我们的代码各组件间的信息传递不依赖特定的组件。