Javascript设计模式总结之 -- 策略模式

FQQ

1.写设计模式系列文章的背景

本系列文章主要是为了对javascript的设计模式做一个总结。各个网站中对设计模式都有介绍,也有专门的书。那么我写的原因是为了自己能够更清晰的理解,也为了对大家的学习有些不同角度的感悟。

我为什么会对设计模式越来越重视?是因为长时间的写代码后,需要再返回来看看自己之前写的代码跟现在的代码有什么不一样,或者说当前的代码比半年前、一年前好很多么? 如果没有感觉好很多,那么说明可能已经到达了这个阶段的天花板,需要拓宽自己的代码视野了。

拓宽自己的视野方式很多,比如阅读好框架的源码,能从中学到不少,但是我通过实践发现,阅读好的源码跟设计模式、算法都是相辅相成的,如果不熟悉各种设计模式和算法,那么框架源码中的一些写法就可能看不明白,或者只能在'照葫芦画瓢',无法理解透彻。这些感悟都是自己采坑踩出来的,所以我开始重视设计模式的深入研究,并力争不断的运用到代码中,这也是一个不断进步的过程,不是一蹴而就。所以大家有兴趣的话,也加入到设计模式的熟悉中来吧。

上面的背景我唠叨的差不多了,在后面的文章中,我会陆续把我知道的和运用过的设计模式写出来。 今天是设计模式的第一课-- 策略模式

模式是什么? 模式就像在建房子的时候,按照之前的经验,先打地基,再搭框架,再填充墙砖,有先后,有重点...有了模式,建多高,多复杂的建筑,都不会出问题。 同时,模式并不是自己从头想出来的,模式更像是站在巨人的肩上,把前人积累出的经验活学活用。

如果没有模式,在初期也能勉强达到效果,比如我们每家都有衣服,如过衣服不是很多,我们随便堆在角落,使用的时候能找到。但如果衣服越来越多,有大人的,也有孩子的,那么找一件衣服,就需要耗费越来越多的时间和成本。所以衣柜的作用,能帮我分类,减少查找时间。同样,加入设计模式能够让代码越来越有条理性,虽然在使用时不能随心所欲的写,但是等到随着项目越来越大,时间越来越久的时候,好的设计模式能大大减少维护成本。所以代码和日常生活都是相通的。

再唠叨一句,好的代码是什么样的?面向对象的基本特性就是 继承, 封装, 多态。落实到具体代码中,我们也在遵循一些最佳实践:

  • 函数功能单一化,输入和输出结果的可预测。
  • 代码逻辑稳定,尽量以后的增删改不对原来的代码做过多修改,因为对原有代码改的越多,越容易影响项目的稳定(OCP 开放-封闭原则)
  • 通过继承更好的做复用,但是不要继承层级过多
  • 。。。。

策略模式

策略模式的定义是:"定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。" 其实有点难懂,我理解可以从实际的生活中看这个策略问题。 比如我们每天上班的时候,可以坐公交、可以坐地铁、可以开车,可以骑自行车,这其实就是一种策略选择问题,无非看的是经济成本和时间成本。

代码中也一样,因为人的思维还是喜欢线性,所以,在写代码的时候,如果没有经过训练,就愿意写一个巨大无比的方法,也不愿管有什么策略,都柔在一起,一堆if else 但是有些编程经验的的同学就知道这样写刚开始时比较爽,但是随着逻辑越来越庞大的时候,就会发现维护起来越来越麻烦,体现在:

  • 忘了之前的这一大堆逻辑其中各个的作用是什么,继续加逻辑的时候就不好下手
  • 每次增加,都可能对以前的逻辑产生负面影响,可能引入新bug

所以,我们会自觉的把逻辑进行功能性的拆分,其实这已经加入策略在里面了。

我理解的策略模式最佳使用场景:

1. 业务逻辑需要在几种算法中动态的进行选择
2. 当一个对象有很多的行为,但不想使用一堆条件判断语句来进行区分

案例1

比如说在app中,都有做任务提高用户存留的游戏或者活动,现有以下活动:

  1. 如果联系签到7天,可以在买某个物品时享受9.9折的优惠,
  2. 如果联系签到14天,某个物品则可享受9折优惠,
  3. 如果联系签到30天,某个物品则在享受8.8折优惠

按照正常思维逻辑,绝大部分程序猿,包括之前的我,都会用条件判断的方式来实现:

function discount(day, price) {
    switch (day) {
        case 7:
            return price * 0.99;
        case 14:
            return price * 0.9;
        case 30:
            return price * 0.88
    }
}

上面的这个discount函数中的各个算法在活动不会改变的情况下没有任何不妥,但是如果现在要增加一个连续签到40天的,并且把连续签到7天的活动删除,那么我们在当前逻辑下,只能更改原函数。这就有违 开放-封闭原则。也就是说类、函数等应该是可以扩展的,但是不可修改

那么根据策略模式,更好的实现可以是这样:

   
    function Discount() {
        // ...
        // 各种逻辑...
        
    }
    
    Discount.rules = {
        day7: 0.99,
        day14:  0.9,
        day30: 0.88
    }
    Discount.addRule = function(day, rule) {
        Discount.rules[day] = rule
    }
    Discount.deleteRule = function(day) {
        delete Discount.rules[day];
    }
    Discount.prototype.getPrice = function(day, price) {
        return price * Discount.rules[day];
    }
    // 添加一个新的折扣规则
    Discount.addRule('day10', 0.95);
    
    var hat = new Discount();
    hat.getPrice('day7', 1000);
    
    
    hat.getPrice('day10', 1000);

通过这种方式,以后的添加或删除,不需要对原函数进行直接的更改。对函数的封装和复用性会更好一些。

当然,我认为这些封装是根据需求的不断迭代而迭代的,如果项目逻辑就是非常简单,后面不会进行逻辑的增删改,那纯粹套这个模式也没什么必要。

案例二

除了算法可以使用策略模式,像代码中的一些逻辑判断,也可以使用,总之目的就是减少对原逻辑的暴力修改,提升逻辑的稳定性和向下兼容性。

网上也有不少例子,比如说在表单验证的时候,可能由于有多个input框,需要多个if else来进行验证。

// 伪代码
function validator() {
    var nameInput = this.nameValue;
    var ageInput = this.ageValue;
    var telInput = this.telValue;
    var addressInput = this.addressValue;
    var simpleRegExp = /^1[3456789]\d{9}$/
    
    if (nameInput === '') {
        this.ErrorMsg = '请填写姓名';
    }
    else if (ageInput === '' || typeof ageInput === 'number') {
       this.ErrorMsg = '请正确填写年龄'
    }
    else if (!simpleRegExp.test(telInput)) {
        this.ErrorMsg = '请填写正确的手机号'
    }
    else if (addressInput === '') {
        this.ErrorMsg = '请填写地址'
    }
}

再次声明,我不认为上面的伪代码就不好,而是什么场景下,用什么样的模式更为易扩展和易维护.

那么什么情况下使用策略模式比较好?还是需要动态增减,而不想在原来代码中改来改去(这简直是重度代码洁癖者的福音有木有)

// 虽然这一堆if else并不是算法,但也可以包装起来,做到更好的OCP


function Validator() {
    this.options =  {
        validateName: function(name) {
            return name.trim() !== '';
        },
        validateAge: function(age) {
            return typeof age === 'number';
        },
        validateTel: function(tel) {
            return /^1[3456789]\d{9}$/.test(tel);
        }
    }
}
// 动态添加规则
Validator.prototype.addPattern = function(valiName, pattern) {
    if (valiName in this.options) {
        return;
    }
    this.options[valiName] = pattern;
}
// 具体进行规则匹配
Validator.prototype.validate = function(args) {
    // {validateName: 'aaa', validateAge: 24, validateTel: '1234567'}
    var type = Object.prototype.toString.call(args);
    var targetOptions = {};
    if (type !== '[object Object]') {
        throw new TypeError('expect type object, but got' + type);
    }
   
    var keys = Object.keys(args);
    for(var i =0; i< keys.length; i++) {
        if (keys[i] in this.options) {
            if(!this.options[keys[i]](args[i])){
                // 如果某个验证失败
                 return false;
            }
        } else {
            throw new Error('validator pattern is not found !');
        }
    }
    return true;
    
}

var va = new Validator();
va.validate({validateName: 'aaa', validateAge: 24, validateTel: '1234567'}) // false

可以看到,比一般写法要麻烦不少,但是以后维护起来会方便很多。所以要不要用这个模式,还需要看具体需求。而且,这个模式在我看来也并不是一劳永逸的解决向下兼容问题。

比如说之前有个验证规则,在线上正常跑着,然后我们某个同学因为不知道,调用了delete方法,把这个验证规则干掉了,这时,也就会出错,无法保证向下兼容。

所以,代码逻辑划分的粒度需要根据实际需要来处理。 后面将会讲 装饰器模式,这个模式在特定的情况下可能做到比较好的向下兼容。且看下一篇~~

阅读 209
1 声望
0 粉丝
0 条评论
1 声望
0 粉丝
文章目录
宣传栏