设计模式是可重用的用于解决软件设计中一般问题的方案。设计模式如此让人着迷,以至在任何编程语言中都有对其进行的探索。
其中一个原因是它可以让我们站在巨人的肩膀上,获得前人所有的经验,保证我们以优雅的方式组织我们的代码,满足我们解决问题所需要的条件。设计模式同样也为我们描述问题提供了通用的词汇。
这比我们通过代码来向别人传达语法和语义性的描述更为方便。
策略模式
策略模式 (Strategy Pattern)又称政策模式,其定义一系列的算法,把它们一个个封装起来,并且使它们可以互相替换。封装的策略算法一般是独立的,策略模式根据输入来调整采用哪个算法。关键是策略的实现和使用分离
策略模式的通用实现
根据上面的例子提炼一下策略模式,折扣计算方式可以被认为是策略(Strategy),这些策略之间可以相互替代,而具体折扣的计算过程可以被认为是封装上下文(Context),封装上下文可以根据需要选择不同的策略。
主要有下面几个概念:
Context:封装上下文,根据需要调用需要的策略,屏蔽外界对策略的直接调用,只对外提供一个接口,根据需要调用对应的策略;Strategy:策略,含有具体的算法,其方法的外观相同,因此可以互相代替;StrategyMap:所有策略的合集,供封装上下文调用;
基于策略模式的程序至少有两部分组成:策略类和环境类(Context)。
- 策略类封装了具体的算法,并负责具体的计算过程,可以理解为“执行者”。
- 环境类(Context)接受客户的请求,然后将请求委托给一个策略类,可以理解为“调度者”。
代码实现
具体的例子我们用编程上的例子来演示,比较好量化。
场景是这样的,某个电商网站希望举办一个活动,通过打折促销来销售库存物品,有的商品满 100 减 30,有的商品满 200 减 80,有的商品直接 8 折出售(想起被双十一支配的恐惧),这样的逻辑交给我们,我们要怎样去实现呢。
通过判断输入的折扣类型来计算商品总价的方式,几个 if-else 就满足了需求,但是这样的做法的缺点也很明显:
- priceCalculate 函数随着折扣类型的增多,if-else 判断语句会变得越来越臃肿;
- 如果增加了新的折扣类型或者折扣类型的算法有所改变,那么需要更改 priceCalculate 函数的实现,这是违反开放-封闭原则的;
- 可复用性差,如果在其他的地方也有类似这样的算法,但规则不一样,上述代码不能复用;
我们可以改造一下,将计算折扣的算法部分提取出来保存为一个对象,折扣的类型作为 key,这样索引的时候通过对象的键值索引调用具体的算法:
这样算法的实现和算法的使用就被分开了,想添加新的算法也变得十分简单:
单例模式
单例模式 (Singleton Pattern)又称为单体模式,保证一个类只有一个实例,并提供一个访问它的全局访问点。也就是说,第二次使用同一个类创建新对象的时候,应该得到与第一次创建的对象完全相同的对象。
单例模式之所以这么叫,是因为它限制一个类只能有一个实例化对象。经典的实现方式是,创建一个类,这个类包含一个方法,这个方法在没有对象存在的情况下,将会创建一个新的实例对象。如果对象存在,这个方法只是返回这个对象的引用。
在JavaScript语言中, 单例服务作为一个从全局空间的代码实现中隔离出来共享的资源空间是为了提供一个单独的函数访问指针。
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式的通用实现
用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象。
主要有下面几个概念:
Singleton:特定类,这是我们需要访问的类,访问者要拿到的是它的实例;instance:单例,是特定类的实例,特定类一般会提供 getInstance 方法来获取该单例;getInstance:获取单例的方法,或者直接由 new 操作符获取;
这里有几个实现点要关注一下:
- 访问时始终返回的是同一个实例;
- 自行实例化,无论是一开始加载的时候就创建好,还是在第一次被访问时;
- 一般还会提供一个 getInstance 法用来获取它的实例;
不好的地方:
创建对象只能通过getInstance创建,增加了这个类的不透明性。Singleton的使用者必须知道这是一个单例类,跟以往通过new方式来获取对象不同。
代码实现
浏览器中的 window 和 document 全局变量,这两个对象都是单例,任何时候访问他们都是一样的对象,window 表示包含 DOM 文档的窗口,document 是窗口中载入的 DOM 文档,分别提供了各自相关的方法。
假如我们现在有这样一个需求:
我们需要对一个全局的数据对象进行管理,这个对象只能有一个,如果有多个会导致数据不同步。
这个需求要求全局只有一个数据存储对象,是典型的适合单例模式的场景,我们可以直接套用上面的代码模板,但是上面的代码模板获取instance必须要调getInstance才行,要是某个使用者直接调了Singleton()或者new Singleton()就会出问题,这次我们换一种写法,让他能够兼容Singleton()和new Singleton(),使用起来更加傻瓜化:
上述代码支持使用new store()的方式调用,我们使用了一个静态变量instance来记录是否有进行过实例化,如果实例化了就返回这个实例,如果没有实例化说明是第一次调用,那就把this赋给这个这个静态变量,因为是使用new调用,这时候的this指向的就是实例化出来的对象,并且最后会隐式的返回this。
如果我们还想支持store()直接调用,我们可以用前面工厂模式用过的方法,检测this是不是当前类的实例,如果不是就帮他用new调用就行了:
发布订阅模式
发布-订阅模式 (Publish-Subscribe Pattern, pub-sub)又叫观察者模式(Observer Pattern),它定义了一种一对多的关系,让多个订阅者对象同时监听某一个发布者,或者叫主题对象,这个主题对象的状态发生变化时就会通知所有订阅自己的订阅者对象,使得它们能够自动更新自己。
描述
- 发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
- 发布—订阅模式可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。
- 发布—订阅模式可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。
- 发布—订阅模式让两个对象松耦合地联系在一起,虽然不太清楚彼此的细节,但这不影响它们之间相互通信。当有新的订阅者出现时,发布者的代码不需要任何修改;同样发布者需要改变时,也不会影响到之前的订阅者。只要之前约定的事件名没有变化,就可以自由地改变它们。
使用场景:跨层级通信、事件绑定等
订阅模式的通用实现
主要有下面几个概念:
Publisher:发布者,当消息发生时负责通知对应订阅者Subscriber:订阅者,当消息发生时被通知的对象SubscriberMap:持有不同 type 的数组,存储有所有订阅者的数组type:消息类型,订阅者可以订阅的不同消息类型subscribe:该方法为将订阅者添加到 SubscriberMap 中对应的数组中unSubscribe:该方法为在 SubscriberMap 中删除订阅者notify:该方法遍历通知 SubscriberMap 中对应 type 的每个订阅者
现在的结构如下图
下面使用通用化的方法实现一下。
首先我们使用立即调用函数 IIFE(Immediately Invoked Function Expression) 方式来将不希望公开的 SubscriberMap 隐藏,然后可以将注册的订阅行为换为回调函数的形式,这样我们可以在消息通知时附带参数信息,在处理通知的时候也更灵活:
代码实现
Vue 实现有一套事件机制,其中一个我们熟知的用法是 EventBus。在多层组件的事件处理中,如果你觉得一层层$on、$emit比较麻烦,而你又不愿意引入 Vuex,那么这时候推介使用 EventBus 来解决组件间的数据通信:
实现组件间的消息传递,不过在中大型项目中,还是推介使用 Vuex,因为如果 Bus 上的事件挂载过多,事件满天飞,就分不清消息的来源和先后顺序,对可维护性是一种破坏。
中介者模式
中介者模式 (Mediator Pattern)又称调停模式,使得各对象不用显式地相互引用,将对象与对象之间紧密的耦合关系变得松散,从而可以独立地改变他们。核心是多个对象之间复杂交互的封装。
根据最少知识原则,一个对象应该尽量少地了解其他对象。如果对象之间耦合性太高,改动一个对象则会影响到很多其他对象,可维护性差。复杂的系统,对象之间的耦合关系会得更加复杂,中介者模式就是为了解决这个问题而诞生的。
你曾见过的中介者模式 在类似的场景中,有以下特点:
- 相亲各方/房源买家卖家(目标对象)之间的关系复杂,引入媒人/中介(中介者)会极大方便各方之间的沟通;
- 相亲各方/房源买家卖家(目标对象)之间如果有什么想法和要求上的变动,通过媒人/中介(中介者)就可以及时通知到相关各方,而目标对象之间相互不通信;
中介者模式的通用实现
对于上面的例子,家长们相当于容易产生耦合的对象(最早的一本设计模式书上将这些对象称为同事,这里也借用一下这个称呼,Colleague),而媒人就相当于中介者(Mediator)。在中介者模式中,同事对象之间互相不通信,而只与中介者通信,同事对象只需知道中介者即可。主要有以下几个概念:
Colleague: 同事对象,只知道中介者而不知道其他同事对象,通过中介者来与其他同事对象通信;Mediator: 中介者,负责与各同事对象的通信;
结构图如下:
可以看到上图,使用中介者模式之后同事对象间的网状结构变成了星型结构,同事对象之间不需要知道彼此,符合最少知识原则。如果同事对象之间需要相互通信,只能通过中介者的方式,这样让同事对象之间原本的强耦合变成弱耦合,强相互依赖变成弱相互依赖,从而让这些同事对象可以独立地改变和复用。原本同事对象间的交互逻辑被中介者封装起来,各个同事对象只需关心自身即可。
中间人模式的一种简单的实现可以在下面找到,publish()和subscribe()方法都被暴露出来使用:
代码实现
购买商品
假设我们正在开发一个购买手机的页面,购买流程中,可以选择手机颜色以及输入购买数量,同时页面中可以对应展示输入内容,另外一个下拉列表框,代表手机内存。还有一个按钮动态显示下一步操作(该颜色库存量充足,显示下一步;否则显示库存不足)
思路:引入中介者模式,所有的节点对象只跟中介者通信。
当下拉选择框selColor、selMemory或者文本框selNum发生了事件行为时,仅通知中介者他们被改变了,同时把自己当做参数传入中介者,以便中介者辨别是谁发生了改变,剩下的事情交给中介者对象来完成。
小结
后续还会陆续更新设计模式之外观模式,状态模式,适配器模式,责任链模式,装饰着模式等,大家有兴趣的可以点赞加关注哦!👍