说来惭愧,我的本科java老师是刘伟老师,他在早些年写过很多关于设计模式的书籍而我一直没有重视。后来随着工程化,代码复杂化的增加,越发觉得设计模式重要性。所以这篇文章就简单的学习并实现一些前端的设计模式。

场景一: 权限判断

这个场景其实很常见,比如一个论坛你需要进到一个板块看一篇文章,首先你未登录是不能看的,另外你登陆了也不一定能看,可能还首先于你的用户组,还有一些其他的因素诸如Vip等等。
So How to deal with it?如果换成很久以前,大一大二的时候来写这段逻辑可能很多人包括我会直接 if-else 一把梭,代码长得像以下这样。
//if-else
function AuthCheck(data) {
if (!data.isLogin) {
console.log("用户未登录!");
return false;
}
if (data.UserGroup !== "some groups") {
console.log("用户组过低!");
return false;
}
if (!data.vip) {
console.log("用户不是Vip!");
return false;
}
}
这段代码的弊端是显而易见的:
- 后期如果需要加入一些策略项时必须改变AuthCheck源码,不符合开闭原则
- 策略项无法复用
那么为了解决这些弊端,策略模式就应运而生了
策略模式
定义 : 要实现某一个功能,有多种方案可以选择。我们定义策略,把它们一个个封装起来,并且使它们可以相互转换。 大概就是“策略+组合”的模式。
首 先我们要实现一个策略组(strategies),实现代码如下:
//strategies
let strategies = {
checkLoginState: (val) => {
if (val === "true") return true;
return false;
},
checkGroups: (val) => {
if (val === "some groups") return true;
return false;
},
checkVip: (val) => {
if (val === "true") return true;
return false;
},
};
特别值得一提的是,这个策略组中的状态都是可以被复用的,同时在后期进行添加拓展的时候也非常方便。
接下来我们要实现一个组合,在这个场景中其实就是一个校验器,这里用到了ES6的类(Class),代码如下:
//Validator
class Validator {
constructor() {
this.strategiesList = [];
}
add(method, val) {
this.strategiesList.push(() => {
return strategies[method](val);
});
}
check() {
let len = this.strategiesList.length;
for (let i = 0; i < len; i++) {
let validation = this.strategiesList[i];
let type = validation();
if (!type) {
return false;
}
}
return true;
}
}
最后用户在调用的时候也非常的简单,比如说有一个用户他已经登录,但是他的用户组比较低不足以达到某篇文章的观看门槛而且他不是Vip,这个时候我们就可以利用校验器做一个拦截啦!
//testUnit
let json = {
isLogin:'true',
UserGroup:'level_1',
isVip:'false'
}
let validator = new Validator();
validator.add("checkLoginState", json.isLogin);
validator.add("checkGroups", json.UserGroup);
validator.add("checkVip", json.isVip);
console.log(validator.check());//false
写到这儿,我忽然记起了以前做杏林网校的时候Vue的验证器,当时看源码这不就是典型的策略模式吗?比如表单中的邮箱的正则,手机的正则,用户名的正则,一个个都是单独的可复用的策略,刺激不好玩不?
什么时候用策略模式?
- 各判断条件下的策略相互独立且可复用
- 策略内部逻辑相对复杂
- 策略需要灵活组合
场景二: Trigger
给定一个需求 : 申请成功后,需要触发对应的订单、消息、审核模块对应逻辑

朴素的方法就是写一个申请成功的函数呗。一旦申请成功了就执行这个函数,如下:
//simple
function applySuccess(){
MessageCenter.fetch();
Order.update();
Checker.alert();
}
弊端很明显,首先这个同样不满足开闭原则和单一职责原则,但凡后期加入许多其他的调用操作这个函数只会越来越臃肿以至于无法维护。另外,每个模块都是又不同的人开发的比如消息通知模块是我负责的,订单通知模块是你负责的,我在维护我的测试模块的时候如果你的模块出问题了我还得确保你的模块没有BUG,或者直接把你的模块给注释了,这显然是不靠谱的。
到这儿,发布-订阅者模式就应运而生了。
发布-订阅者模式
我的理解发布订阅这就是去维护一个EventEmit也就是一个消息中心,那么一些action可以使得发布者触发更新(‘Update命令’),让消息中心中所有的订阅者(subscribers)触发自己的回调函数。

那么用订阅-发布者模式如何去修改上述代码呢?
//订阅-发布者模式
class EventEmit {
constructor() {
this.events = {};
}
//on的作用实际上就是将相关元素注册为subscriber
on(actionName, cb) {
if (this.events[actionName]) {
this.events[actionName].push(cb);
} else {
this.events[actionName] = [cb];
}
}
//trigger的作用实际上就是发布者触发一个更新命令
trigger(actionName, ...args) {
if (this.events[actionName]) {
this.events[actionName].forEach((cb) => {
cb(args);
});
}
}
}
可以很明显的看到其内核实质上就是在维护一个event的数组。而后客户端的代码可以这样写:
//testUnit
let eventEmit = new EventEmit();
eventEmit.on("success", () => {
console.log("MessageCenter fetching successfully!");
});
eventEmit.on("success", () => {
console.log("Order updating successfully!");
});
eventEmit.on("success", () => {
console.log("Checker alerting successfully!");
});
eventEmit.trigger('success')
//MessageCenter fetching successfully!
//Order updating successfully!
//Checker alerting successfully!
这样一来是不是就完成了各个模块的解耦了呢?依附于success的事件触发而不依附于其他模块。这种写法是不是跟DOM中的事件监听非常像呢?实际上DOM中的事件模型就是典型的订阅发布者模式,举个例子,我将某些按钮绑定‘click’事件这实际上就是将他们注册为订阅者(见上述on函数),当我真正单机按钮的时候实际上单击事件就充当一个发布者(见上述trigger函数)。
Vue与订阅发布者模式的那些事儿
Vue中有一个很重要的特性就是双向绑定。其实现原理就是数据劫持+订阅发布者模式,Vue2.x用Object中的defineProperty实现对象劫持(Vue3使用了ES6的Proxy,解决了数组变量修改监听不到的问题),在data中所有的getter中写入注册成订阅者的函数,在所有setter中写入通知更新函数(类似于一个trigger),一旦数据发生更改就触发trigger,遍历dep中的eventList中所有的callbackFunction。当然细节肯定没有那么简单,我以后如果有时间会去专门研究一下Vue的源码。
什么时候用发布-订阅模式?
- 各模块相互独立
- 存在一对多的依赖关系
- 依赖模块不稳定、依赖关系不稳定
- 各模块由不同的人员、团队开发
场景三: 赋能
我和老缪在大二的时候参加服务外包一不小心拿了个全国一等奖,但是还好评委们没看老缪写的那部分代码,给你举个例子啊,比如说一个寻找餐厅的函数,老缪给他取名”FindEat”,比如说寻找酒店的函数他给取名”FindBed”诸如此类,可以认为老缪的英语能力为0,那么现在我需要一个模式,给老缪赋能,让他会说流利的英文,咋整呢?
装饰者模式
主要逻辑就是实现一个装饰器进行包装,例子很简单
class Miaoz {
ability() {
console.log("speak Chinese");
}
}
class Decorator {
constructor(old) {
this.old = old;
}
//赋能
Empowerment() {
console.log("speack English fluently");
}
//装饰之后的对象
newPerson() {
this.old.ability();
this.Empowerment();
}
}
let miaoz = new Miaoz();
let new_miaoz = new Decorator(miaoz);
new_miaoz.newPerson()
//speak Chinese
//speack English fluently
场景四: 代理
ES6中有个东西叫做Proxy,他能代理一个对象并且支持很多很多的方法API比如set/get等等。
周杰伦的演唱会门票买不到,但是我是十年jay粉,我如果想去看那么找黄牛买门票我还是愿意的,在这里黄牛就是一个代理。
小时候玩刷钻有个东西叫做”花刺代理IP”,你能代理别人的IP以别人的身份进行互联网访问…
这些东西都叫做代理, 我们不直接操作原有对象,而是委托代理者去进行。代理者的作用,就是对我们的请求预先进行处理或转接给实际对象。
比如说很常见的邮件过滤,就可以运用到代理模式。
const banList = ["qq.com", "gmail.com", "126.com"];
function SMTP(email) {
//SMTP逻辑...
console.log(email + " has been sent!");
}
class ProxyEmail {
constructor(email, banList) {
this.email = email;
this.banList = banList;
}
sendEmail() {
if (this.banList.indexOf(this.email)) {
SMTP(this.email);
} else {
console.log("ur email has been banned...");
}
}
}
let test = new ProxyEmail("qq.com", banList);
test.sendEmail();
//ur email has been banned...
什么时候用代理模式?
- 模块职责单一且可复用
- 两个模块间的交互需要一定限制关系
场景四:单例模式
这个在java里面比较常见,有一些函数封装在一个对象中,每一次去调用实际上我们没必要每一次都去new一个对象出来,我们可以借由这种单例模式来创造一个独一无二得单例,这样子方便管理得同时也减少了资源浪费。
class singleton {
constructor() {
this.instance = null;
}
init() {
function publicfuncs() {
console.log("this is a public funcs");
}
return {
publicfuncs: publicfuncs,
};
}
getInstance() {
if (!this.instance) {
this.instance = this.init();
}
return this.instance;
}
}
let singleton1 = new singleton();
let instance1 = singleton1.getInstance()
let instance2 = singleton1.getInstance()
console.log(instance1===instance2);
instance1.publicfuncs()
结尾
其实设计模式就是程序届的套路,活用设计模式可以使得代码变得更加规范并且可以维护,这是每个程序人的梦想。但是不是每一种逻辑都要强行套用设计模式,其实还有很多模式没有提到比如很著名的单例模式,工厂模式等等。希望自己一直能有理解~