代码要写成别人看不懂的样子(十)


本篇文章参考书籍《JavaScript设计模式》–张容铭

文章目录

    • 前言
    • 状态模式
    • 策略模式

前言

??各位写代码的时候,经常会出现条件判断吧,那么条件判断里面脸最熟的当属 if 了吧,这个东西在我们开始编码的时候,真的是隔一段时间不写,就浑身难受。

??但是呢,if 这个东西,哪怕是再简单的判断,也会有隐含问题,而且在代码可读性上,条件判断的效果很不友好,当存在多重判断的时候,简直就是灾难。

??所以为了解决条件判断的这些弊端,状态模式应运而生。

状态模式

??状态模式:当一个对象的内部状态发生改变时,会导致其行为的改变,这看起来像是改变了对象。

??我们可以将不同的状态结果封装在状态对象内部,然后该状态对象返回一个可被调用的接口方法,用于调用状态对下内部某种方法。做法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//状态对象
var ResultState = function() {<!-- -->
    //结果保存在内部状态中
    var States = {<!-- -->
        //每种状态作为一种独立方法保存
        state0:funciton() {<!-- -->
            //处理结果0
            console.log('这是第一种情况');
        },
        state1:funciton() {<!-- -->
            //处理结果1
            console.log('这是第二种情况');
        },
        state2:funciton() {<!-- -->
            //处理结果2
            console.log('这是第三种情况');
        },
        state3:funciton() {<!-- -->
            //处理结果3
            console.log('这是第四种情况');
        },
    }
    //获取某一种状态,并执行其方法
    function show(result) {<!-- -->
        States['state' + result] && States['state' + result]();
    }
    return {<!-- -->
        //返回调用状态方法接口
        show: show
    }
} ();

??当我们想调用第三种结果的时候,我们就可以按照下面这种方法实现。

1
2
//展示结果3
ResultState.show(3);

??上面方法展示了状态模式的基本雏形,该模式主要目的就是将条件判断的不同结果转化为状态对象的内部状态。由于是内部状态,所以创建时设置成私有变量,只提供一个接口给外部进行增删改查,方便了我们对状态对象中,内部对象的管理。

??大家小时候都玩过超级玛丽吧,我们操作人物的时候可以跳跃,奔跑,蹲下,丢飞镖等,这些都是一个一个的状态,如果我们用 if 或者 switch 条件判断的话,那么后期会相当难维护,因为增加或者删除一个状态需要修改的地方太多了,这时使用状态模式帮我们管理就再合适不过了。

??对于超级玛丽,有的时候需要跳起来丢飞镖,有的时候需要蹲下丢飞镖,有的时候需要跑起来丢,这些组合状态如果用 if 或者 switch 判断的话,无形当中增加的成本是无法想象的。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//单动作条件判断 每增加一个动作就需要添加一个判断
var lastAction = '';
function changeMarry(action) {<!-- -->
    if(action = 'jump') {<!-- -->
        //跳跃动作
    } else if(action = 'move') {<!-- -->
        //移动动作
    } else {<!-- -->
        //默认情况
    }
    lastAction = action;
}
//符合动作对条件判断的开销是翻倍的
var lastAction1 = '';
var lastAction2 = '';
function changeMarry(action1, action2) {<!-- -->
    if(action1 = 'throw') {<!-- -->
        //丢飞镖
    } else if(action1 = 'jump') {<!-- -->
        //跳跃
    } else if(action1 = 'jump' && action2 = 'throw') {<!-- -->
        //跳起来扔飞镖
    } else if(action1 = 'move' && action2 = 'throw') {<!-- -->
        //移动中扔飞镖
    }
    //保留上一个动作
    lastAction1 = action1 || '';
    lastAction2 = action2 || '';
}

??上面代码的可维护性和可读性都是很差的,日后对于新动作的添加或者修改原有动作的成本都是很大的。那么为了解决这一问题我们引入状态模式。解决思路如下:

??1.创建一个状态对象。

??2.内部保存状态变量。

??3.内部封装好每种动作对应的状态。

??4.状态对象返回一个接口对象,可以对内部状态进行修改或者调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
//创建超级玛丽状态类
var MarryState = function() {<!-- -->
    //内部状态私有变量
    var _currentState = {<!-- -->},
        //动作与状态方法映射
        states = {<!-- -->
            jump: function() {<!-- -->
                //跳跃
                console.log('jump');
            },
            move: function() {<!-- -->
                //移动
                console.log('move');
            },
            throw: function() {<!-- -->
                //丢飞镖
                console.log('throw');
            },
            squat: function() {<!-- -->
                //蹲下
                console.log('squat');
            }
        };
    //动作控制类
    var Action = {<!-- -->
        //改变状态方法
        changeState: function() {<!-- -->
            //组合动作通过传递多个参数实现
            var arg = arguments;
            //重置内部状态
            _currentState = {<!-- -->};
            //如果有动作则添加动作
            if(arg.length) {<!-- -->
                //遍历动作
                for(var i = 0; len = arg.length; i < len; i++) {<!-- -->
                    //向内部状态中添加动作
                    _currentState[arg[i]] = true;
                }
            }
            //返回动作控制类
            return this;
        },
        //执行动作
        goes: function() {<!-- -->
            console.log('触发一次动作');
            //遍历内部状态保存的动作
            for(var i in currentState) {<!-- -->
                //如果该动作存在则执行
                states[i] && states[i]();
            }
            return this;
        }
    }
    //返回接口方法change ,goes
    return {<!-- -->
        change: Action.changeState,
        goes: Action.goes
    }
}

??超级玛丽的状态创建完成了,接下来就是使用了。

1
2
3
4
5
6
7
8
//创建一个超级玛丽
var marry = new MarryState();
marry
    .change('jump', 'throw')  //添加跳跃与丢飞镖动作
    .goes()          //执行动作
    .goes()          //执行动作
    .change('throw') //添加丢飞镖动作
    .goes();         //执行动作

??输出结果如下:

1
2
3
4
5
6
7
8
//触发一次动作
//jump
//throw
//触发一次动作
//jump
//throw
//触发一次动作
//throw

??改变状态类一个状态,就改变了状态对象的执行结果,是不是有点类似对象,让这些状态管理一下清晰了起来。

策略模式

??各位最近刚过完双十一吧,参与了将近五千亿的项目,感觉怎么样?凑满减的时候是不是感觉自己的数学水平又重新到达巅峰了。双十一活动有很多哈,比如满300减40,满200减25等等,有的商品不参与满减活动,但是可以先付定金,然后再付尾款,这一点像不像上面的刚介绍的状态模式,会用到很多条件判断。

??但是,情况又不太一样,不管商品采用哪种方案,一般都会只选取其中一种,比如该商品满300减40了,就不参与满200减25的活动了,如果使用状态模式的话,那就会为每一种商品,创建一个状态对象,这样做就会产生代码冗余。

??这个时候就需要用策略模式来解决当前问题了。策略模式结构上与状态模式很像,都是再内部封装一个对象,然后返回的接口对象实现对内部对象的调用。

??不同点是,策略模式不需要管理状态,状态间没有依赖关系,策略之间可以相互替换,在策略对象内部保存的是相互独立的一些算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//价格策略对象
var PriceStrategy = function() {<!-- -->
    //内部算法对象
    var strategy= {<!-- -->
        //满300减40
        reduce40: function(price) {<!-- -->
            return +price - ~~(price / 300) * 40; //~~表示parseInt
        },
        //满200减25
        reduce25: function(price) {<!-- -->
            return +price - ~~(price / 200) * 25;
        },
        //定金50付尾款
        deposit: function(price) {<!-- -->
            return +price - 50;
        },
    }
    //策略算法调用接口
    return function(algorithm, price) {<!-- -->
        //如果算法存在则调用算法,否则返回false
        return strategy[algorithm] && strategy[algorithm](price)
    }
} ();

??策略对象已经写出来了,我们可以看看怎么获取。

1
2
var price = PriceStrategy('reduce40', 1000);
console.log(price);    // 880

??策略模式其实应用的场景很多,比如 JQuery 动画中的缓冲函数,验证表单时候的正则算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//表单正则策略对象
var InputStrategy = function() {<!-- -->
    var strategy = {<!-- -->
        //是否为空
        notNull: funetion(value) {<!-- -->
            return /\s+/.test(value) ? '请输入内容' : '';
        },
        //是否是一个数字
        number: funetion(value) {<!-- -->
            return /^[0-9]+(\.[0-9]+)?$/.test(value) ? '' : '请输入数字';
        },
    }
    return {<!-- -->
        //验证接口 type 算法 value 表单值
        check: function(type, value) {<!-- -->
            //去除收尾空白符
            value = value.replace(/^\s+|\s+$/g, '');
            return strategy[type] ? strategy[type](value) : '没有该类型检测方法';
        },
        addStrategy: function(type, fn) {<!-- -->
            strategy[type] = fn;
        }
    }
} ();

??上面例子中,我们新增加了一个 addStrategy 用来增加新的策略,这个接口可以帮我们不用修改策略对象内部方法,便可以新增新的策略。用法如下:

1
2
3
4
//拓展可延续算法
InputStrategy.addStrategy('nickname', function(value) {<!-- -->
    return /^[a-zA-Z]\w{<!-- -->3,7}$/.test(value) ? '' : '请输入4-8位昵称, 如:LHXX';
});

??本片文章的两个设计模式还是很常见的,也比较简单,大家用一两次就能学会,使用后会给以后需求增加带来很多方便。