前言
大家好,这是第三篇作者对于设计模式的分享了,前两篇可以参考:
手写一下JavaScript的几种设计模式 (工厂模式,单例模式,适配器模式,装饰者模式,建造者模式)
用英雄联盟的方式讲解JavaScript设计模式(一)! (构造函数模式,外观模式,代理模式)
设计模式在编程开发中用途十分广泛,每一个模式描述了一个在我们周围不断重复发生的问题,以及解决问题的核心!很多的时候,对于我们其实如何选择适合的设计模式,才更加消耗时间。从之前的文章,每一个设计模式都会有一到两个例子,既可以给自己以后开发回忆设计模式提供帮助,也希望可以给读者一些启发。
策略模式
简介
策略模式定义了算法家族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化不会影响到使用算法的客户。
那听起来云山雾绕,怎么都涉及到 算法 了 ?难道我一个前端是时候进攻算法大军了吗。其实并不是,用一个超级常见的例子就可以解释!
让我们又回到英雄联盟,当我们第一次登陆英雄联盟的时候,需要输入一个新的姓名吧?起名规则起码得有以下这几条:
- 名字长度
- 名字是否有非法字符
- 是否重名
- 不能为空
其中具体的设定,只有开发者才知道了,身为玩家只能注意到这几点,那策略模式怎么体现在这里的呢?首先我们实现一个显而易见功能的例子:
var validator = {
validate: function (value, type) {
switch (type) {
case 'isNonEmpty ':
{
return true; // 名字不能为空
}
case 'isNoNumber ':
{
return true; // 名字 不是 纯数字
break;
}
case 'isExist ':
{
return true; // 名字已存在
}
case 'isLength':
{
return true; // 长度合理
}
}
}
};
复制代码
上述代码可以实现一个表单验证系统,刚创建角色起名字的时候验证那里的功能,只需要传入相应的参数就可以。
validator.validate('测试名字', 'isNumber') // false
虽然可以得到理想的结果,但这种写法有十分严重的缺点,最重要的,每次增加或修改规则时,需要修改整个validate函数,这不符合开放封闭原则,增加逻辑,让函数更加复杂不可控。
那真正适合的代码应该怎么写呢?
var validator = {
// 所有验证规则处理函数存放的地方
types: {},
validate: function (str, types) {
this.messages = [];
var checker, result, msg, i;
for (i in types) {
var type = types[i];
checker = this.types[type]; // 获取验证规则的验证类
if (!checker) { // 如果验证规则类不存在,抛出异常
throw {
name: "ValidationError",
message: "No handler to validate type " + type
};
}
result = checker.validate(str); // 使用查到到的单个验证类进行验证
if (!result) {
msg = "Invalid value for *" + type + "*, " + checker.instructions;
this.messages.push(msg);
}
}
return this.hasErrors();
},
// 是否有message错误信息
hasErrors: function () {
return this.messages.length !== 0;
}
};
复制代码
上面的代码定义了validator对象以及validate函数,函数内部会对传入的字符串,检测类型数组进行处理。如果存在规则,进行判断,并把错误信息发送到this.message。如果不存在规则,自然的就不需要继续执行,抛出error即可。
// 验证给定的值是否不为空
validator.types.isNonEmpty = {
validate: function (value) {
return value !== "";
},
instructions: "传入的值不能为空"
};
// 验证给定的值是否 不是 纯数字
validator.types.isNoNumber = {
validate: function (value) {
return isNaN(value); // 伪写法,因为isNaN会误判布尔值和空字符串等,因此并不能作为真正判断纯数字的依据
},
instructions: "传入的值不能是纯数字"
};
// 验证给定的值是否存在
validator.types.isExist = {
validate: function (value) {
// $.ajax() ...
return true;
},
instructions: "给定的值已经存在"
};
// 验证给定的值长度是否合理
validator.types.isLength = {
validate: function (value) {
var l = value.toString().length
if ( l > 2 && l < 10) {
return true;
} else {
return false;
}
},
instructions: "长度不合理,请长度在2-10个字符内"
};
复制代码
上面对types规则进行了补充,定义了几种规则,至此,对于名称校验,简单的设定就敲完了。接下来要准备的就是一个能够在英雄联盟合理的名字进行验证:
var types = ['isExist', 'isLength', 'isNoNumber', 'isNonEmpty']; // 决定想要的规则,无论增加或者减少,原函数都不需要改动
function check (name, types) {
validator.validate(name, types);
if (validator.hasErrors()) {
console.log(validator.messages.join("n"));
} else {
console.log('验证通过!')
}
}
check('okckokckokck', types) // 长度不合理,请长度在2-10个字符内
check('老faker', types) // true
check('00001', types) // 传入的值不能是纯数字
复制代码
首先设定好想要的规则,用一个types数组囊括进来,之后定义一个check函数,把结果处理封装一下,最后传入参数,无论想要检测什么规则,都不需要修改原函数。现在无论我想检测faker可不可以注册,还是一个空字符串,都可以传入规则,进行使用。如果想添加新的规则,只需要在validator.types上续写对象就可以,方便清晰,结构明朗。
核心思想就是把复杂的算法结构,分别封装起来,让他们之间可以互相替换,上面的代码就很好的体现了 互相替换 ,因为无论我怎么去修改想要的规则,都不需要改动原本的代码。
桥接模式
简介
在系统沿着多个维度变化的同时,又不增加其复杂度并已达到解耦。将抽象部分与它的实现部分分离,使它们都可以独立地变化。简单的说:桥接模式最主要的特点是实现层(如元素绑定的事件)与抽象层(如修饰页面UI逻辑)解耦分离。
下面依然是一个例子:
假如我们还在英雄联盟的世界里,每一场游戏最终都会有一个结局,无论胜利还是失败,都会弹出一个窗口,告诉你 —— Victory或者是Defeat。
function GameMessage (type) { // 抽象 与 实现 的 桥梁
this.fn = type ? new Victory() : new Defeat()
}
GameMessage.prototype.show = function() {
this.fn.show()
}
function Defeat() { // 抽象层
this.show = function() {
console.log('im loser')
}
}
function Victory() { // 抽象层
this.show = function() {
console.log('im winner')
}
}
// 实现层
function getResult() {
var switchVD = Math.ceil(Math.random()*10) > 5 // 胜利失败一半一半
return new GameMessage(switchVD)
}
var result1 = getResult()
var result2 = getResult()
var result3 = getResult()
result1.show()
result2.show()
result3.show()
复制代码
首先我们创建了一个GameMessage的函数,我们都知道胜利失败都有一半的概率,因此定义了switchVD变量,模拟一个随机事件,同时每次结果调用一次getResult函数,获取最新结果。
桥接模式体现在GameMessage函数上,将抽象的 Victory() 以及 Defeat() 与 我们获取结果的 getResult() 实现解耦。函数之间不糅合逻辑,但又通过桥梁函数,连接在一起。
这么写的好处就是,两者都可以独立的变化,互不打扰。毕竟如果揉在一起,可能逻辑如下:
function Defeat() { // 抽象层
this.show = function() {
console.log('im loser')
}
}
function Victory() { // 抽象层
this.show = function() {
console.log('im winner')
}
}
var switchVD = Math.ceil(Math.random()*10) > 5
if (switchVD) {
var result = new Victory()
} else {
var result = new Defeat()
}
result.show() // loser or winner
复制代码
上述代码可以轻松的看到,如果没有桥接模式,直接把实现层,渲染层糅合在一起,会依赖上下文。倘若获取不到上下文的环境,很容易出现问题。
小结
桥接模式在日常开发中,会在不经意间频繁使用,目的也是为了让代码结构清晰,将不同逻辑的代码互相解耦。便于日后维护,开发时也更能区分模块,看的舒服,自然效率也高。
桥接模式关键是要理解抽象部分与实现部分的分离,使得二者可以独立的变化,而不必拘泥于形式。灵活的变化,适用场景的多变就非常适合使用这种模式来实现。桥接模式最重要的是找到代码中不同的变化纬度。
状态模式
简介
状态模式(State)允许一个对象在其内部状态改变的时候改变它的行为,对象看起来似乎修改了它的类。 其实就是用一个对象或者数组记录一组状态,每个状态对应一个实现,实现的时候根据状态挨个去运行实现。
优点:
- 一个状态对应一个行为,直观清晰,增改方便。
- 状态与状态间,行为与行为间彼此独立互不干扰。
- 避免对象条件判断语句过多。
- 不用执行不必要的判断语句。
缺点:
- 需要将事物的不同状态以及对应的行为拆分出来,有时候会过度设计。
- 必然会增加事物类和动作类的个数,动作类再根据单一原则,分别拆成几个类,会反而使得代码混乱。
比如下面我们定义一个英雄的状态,名字叫亚索,其中亚索可能同时有好几个状态比如 边走边攻击 —— 我们俗称的“走A”,还有可能释放技能之后接一个“B键回家”的操作,当然最有可能的是eqw闪r行云流水的操作收获一个人头,再接一个ctrl+f6等。
如果对这些操作一个个进行处理判断,需要多个if-else或switch不仅丑陋不说,而且在遇到有组合动作的时候,实现就会更为冗余。那么我们这里的复杂操作,可以使用 状态模式 来实现。
状态模式 的思路是:首先创建一个状态对象或者数组,在对象内部存储需要操作的状态数组或对象,然后状态对象提供一些接口,可以更改状态以及执行动作。
那现在有一个英雄叫做亚索!下面代码,我们就用亚索的状态来实现一下传说中的状态模式:
function YasuoState() {
//存储当前即将执行动作的状态!
this.currentstate = [];
this.Actions = {
walk : function(){
console.log('walk');
},
attack : function(){
console.log('attack');
},
magic : function(){
console.log('magic');
},
backhome : function(){
console.log('backhome');
}
};
}
YasuoState.prototype.changeState = function() {
//清空当前的动作
this.currentstate = [];
Object.keys(arguments).forEach((i) => this.currentstate.push(arguments[i]))
return this;
}
YasuoState.prototype.YasuoActions = function() {
//当前动作集合中的动作依次执行
this.currentstate.forEach((k) => this.Actions[k] && this.Actions[k]())
return this;
}
var yasuoState = new YasuoState();
yasuoState.changeState('walk','attack').YasuoActions().changeState('walk').YasuoActions().YasuoActions();
复制代码
上面代码成功实现了亚索的状态模式,我们假设他有走路、攻击、释放技能、回家几个状态,其中这几个状态其实是可以同时输入指令的,要不然那些职业选手的高光操作就会在 技能衔接 而出现的卡顿 香消玉殒。
状态模式最常见的就是日常的例子 —— 红绿灯,每当切换状态的时候,执行一次动作。
至于英雄联盟中,最常见的就是边走边攻击,在输入命令后,首先改变了我们对象的状态yasuoState.changeState('magic','backhome'),然后因为在代码中有return this;,可以链式调用接下来的行为,于是我们让它依次执行刚才输入的状态。接下来又一次改变了状态changeState('walk'),并且进行执行。可以看到执行了两次,由于状态并没有再次改变,因此只需要重复执行就可以保证我们的英雄一直往前走下去了。
希望状态模式可以帮助你解决绝大多数,需要切换状态的操作。遇到类似的问题时,可以迅速拿出成熟可靠的状态模式解决之。
总结
本次分享的三种模式,都可以在英雄联盟中找到影子,因为我喜欢这款游戏,所以很轻松可以找到其中使用的设计模式:
- 策略模式 —— 行为模式,当业务需要很多种判断,甚至组合计算时,为了方便扩展修改或使用它。
- 桥接模式 —— 结构型模式,构建需要经常扩展功能的对象时,经常会遇到。
- 状态模式 —— 行为模式,希望业务根据状态改变而发生改变,最常见的现实例子就是红绿灯。
设计模式主要可以帮助我们解决,开发中对代码的设计问题,那我们如何找到合适的对象,并应用合适的设计模式呢?
借用书中的几个提示吧:
寻找合适的对象
决定对象的粒度
决定好这个对象设计的接口
把对象需要的具体函数实现
合理的运用代码复用机制
设计的代码应该可以支持变化,要对变化有预见性
大概是这几种,在 javascript 中涉及编译的场景较少,就不叙述了。
设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
根据上面的几条规则,在开发接口和函数的时候,时刻注意,就可以避免大多数代码设计上的问题,对于以后的维护也会有巨大的帮助。下一个接受代码的人,也会十分感激你的,读代码其实和读书一样,你现在偷懒写的代码可能无所谓,后面接手的人会疯狂吐槽。相反如果你优雅的实现,像我,就会心里由衷的佩服,看到整齐的函数,注释明朗的功能,不得不说,高手确实是高手啊,短短 200 行,让人跪服,就突出一个词 —— 优雅。
相关参考
- tom大叔博客
- 《设计可复用的设计模式》
作者:黄梵高
链接: https:// juejin.im/post/5ec791fd f265da770d3daabd