《JavaScript设计模式与开发实践》学习笔记part2-单例模式与策略模式

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u012863664/article/details/79335209

本篇内容主要讲述JavaScript中的两个设计模式:单例模式和策略模式。

第二部分 设计模式

第五章 单例模式

     单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
     单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏 览器中的 window 对象等。在 JavaScript 开发中,单例模式的用途同样非常广泛。试想一下,当我 们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少 次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建。

5.1 实现单例模式

     要实现一个标准的单例模式并不复杂,无非是用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象。代码如下:

var Singleton = function (name) {
    this.name = name;
    this.instance = null;
};
Singleton.prototype.getName = function () {
    alert(this.name);
};
Singleton.getInstance = function (name) {
    if (!this.instance) {
        this.instance = new Singleton(name);
    }
    return this.instance;
};

var a = Singleton.getInstance('sven1');
var b = Singleton.getInstance('sven2');
alert(a === b); // true

或者:

var Singleton = function (name) {
    this.name = name;
};
Singleton.prototype.getName = function () {
    alert(this.name);
};
Singleton.getInstance = (function () {
    var instance = null;
    return function (name) {
        if (!instance) {
            instance = new Singleton(name);
        }
        return instance;
    }
})();

5.2 惰性单例

     假如我们的需求是在一个页面上点击登录按钮之后,弹出一个登录框,而且只能出现同一个登录框,不要创建多个,实现代码如下:

<html>
<body>
    <button id="loginBtn">登录</button>
</body>
<script>
    var createLoginLayer = function () {
        var div = document.createElement('div');
        div.innerHTML = '我是登录浮窗';
        div.style.display = 'none';
        document.body.appendChild(div);
        return div;
    };
    document.getElementById('loginBtn').onclick = function () {
        var loginLayer = createLoginLayer();
        loginLayer.style.display = 'block';
    };
</script>
</html>

上面这段代码,只是实现了惰性的效果(只有点击了按钮之后才会创建那个div,页面加载时不创建),但失去了单例的效果,下面是优化

var createLoginLayer = (function () {
    var div;
    return function () {
        if (!div) {
            div = document.createElement('div');
            div.innerHTML = '我是登录浮窗';
            div.style.display = 'none';
            document.body.appendChild(div);
        }
        return div;
    }
})();
document.getElementById('loginBtn').onclick = function () {
    var loginLayer = createLoginLayer();
    loginLayer.style.display = 'block';
};

5.3 通用的惰性单例

     比如有一个需求是页面上需要展示一个唯一的登录浮框(不需要多次创建)

var createLoginLayer = (function () {
  var div;
  return function () {
      if (!div) {
          div = document.createElement('div');
          div.innerHTML = '我是登录浮窗';
          div.style.display = 'none';
          document.body.appendChild(div);
      }
      return div;
  }
})();
document.getElementById('loginBtn').onclick = function () {
  var loginLayer = createLoginLayer();
  loginLayer.style.display = 'block';
};

上面的代码存在以下两个问题:

  • 这段代码是违反单一职责原则的,创建对象和管理单例的逻辑都放在createLoginLayer对象内部
  • 如果我们下次需要创建页面中唯一的iFrame或者script(用于跨域请求数据),就必须得如法炮制,把createLoginLayer函数几乎照抄一遍
    因此我们需要将管理单例的逻辑从原来的代码中抽离出来,封装在getSingle函数内部,创建对象的方法fn被当成参数动态传入getSingle函数中:
var getSingle = function(fn) {
  var result;
  return function() {
    return result || (result = fn.apply(this, arguments));
  }
}
var createLoginLayer = function() {
  div = document.createElement('div');
  div.innerHTML = '我是登录浮窗';
  div.style.display = 'none';
  document.body.appendChild(div);
  return div;
}
var createSingleLoginLayer = getSingle(createLoginLayer);
document.getElementById('loginBtn').onclick = function() {
  var loginLayer = createLoginLayer();
  loginLayer.style.display = 'none';
}

     通过这样将用于创建登录浮窗的方法用参数fn的形式传入getSingle中,我们不仅可以传入createLoginLayer, 还能传入createScript、createFrame、createXhr等等。之后再让getSingle返回一个新的函数。

     在这个例子中,我们把创建实例对象的职责和管理单例的职责分别放置在两个方法里,这两 个方法可以独立变化而互不影响,当它们连接在一起的时候,就完成了创建唯一实例对象的功能, 看起来是一件挺奇妙的事情。

     在 getSinge 函数中,实际上也提到了闭包和高阶函数的概念。单例模式是一种简单但非常实 用的模式,特别是惰性单例技术,在合适的时候才创建对象,并且只创建唯一的一个。更奇妙的 是,创建对象和管理单例的职责被分布在两个不同的方法中,这两个方法组合起来才具有单例模 式的威力。

第六章 策略模式

6.1 定义

     策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

6.2 使用策略模式计算奖金

     策略模式有着广泛的应用。以年终奖的计算为例进行介绍。
     很多公司的年终奖是根据员工的工资基数和年底绩效情况来发放的。例如,绩效为 S 的人年 终奖有 4 倍工资,绩效为 A 的人年终奖有 3 倍工资,而绩效为 B 的人年终奖是 2 倍工资。假设财 务部要求我们提供一段代码,来方便他们计算员工的年终奖。

var calculateBonus = function (performanceLevel, salary) {
    if (performanceLevel === 'S') {
        return salary * 4;
    }
    if (performanceLevel === 'A') {
        return salary * 3;
    }
    if (performanceLevel === 'B') {
        return salary * 2;
    }
};
calculateBonus('B', 20000); // 输出:40000 
calculateBonus( 'S', 6000 ); // 输出:24000

上面这种写法的缺点不少,有以下这些内容:

  • calculateBonus 函数比较庞大,包含了很多 if-else 语句,这些语句需要覆盖所有的逻辑 分支。(如果类型比较多,会有非常多的if-else语句,看起来非常臃肿)
  • calculateBonus 函数缺乏弹性,如果增加了一种新的绩效等级 C,或者想把绩效 S 的奖金 系数改为 5,那我们必须深入 calculateBonus 函数的内部实现,这是违反开放封闭原则的。
  • 算法的复用性差,如果在程序的其他地方需要重用这些计算奖金的算法呢?我们的选择 只有复制和粘贴。

那么下面是使用策略模式重构出来的代码
     一个基于策略模式的程序至少由两部分组成。第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。 第二个部分是环境类 Context,Context 接受客户的请求,随后把请求委托给某一个策略类。要做到这点,说明 Context 中要维持对某个策略对象的引用。
下面是模拟传统的面向对象语言中的实现:

var performanceS = function () {};
performanceS.prototype.calculate = function (salary) {
    return salary * 4;
}
var performanceA = function () {};
performanceA.prototype.calculate = function (salary) {
    return salary * 3;
};
var performanceB = function () {};
performanceB.prototype.calculate = function (salary) {
    return salary * 2;
};

然后需要定义一个奖金类Bonus:

var Bonus = function () {
    this.salary = null; // 原始工资
    this.strategy = null; // 绩效等级对应的策略对象
};
Bonus.prototype.setSalary = function (salary) {
    this.salary = salary; // 设置员工的原始工资
};
Bonus.prototype.setStrategy = function (strategy) {
    this.strategy = strategy; // 设置员工绩效等级对应的策略对象
};
Bonus.prototype.getBonus = function () { // 取得奖金数额
    return this.strategy.calculate(this.salary); // 把计算奖金的操作委托给对应的策略对象
};

var bonus = new Bonus();

bonus.setSalary(10000);
bonus.setStrategy(new performanceS()); // 设置策略对象
console.log(bonus.getBonus()); // 输出:40000 

bonus.setStrategy( new performanceA() ); // 设置策略对象
console.log(bonus.getBonus()); // 输出:30000

     通过代码再一次回顾一下策略模式的思想:定义一系列的算法,把它们各自封装成策略类,算法被 封装在策略类内部的方法里。在客户对 Context 发起请求的时候,Context 总是把请求委托给这些 策略对象中间的某一个进行计算。

下面是JavaScript版本的策略模式

var strategies = {
    "S": function (salary) {
        return salary * 4;
    },
    "A": function (salary) {
        return salary * 3;
    },
    "B": function (salary) {
        return salary * 2;
    }
};

var calculateBonus = function (level, salary) {
    return strategies[level](salary);
};
console.log(calculateBonus('S', 20000));    // 输出:80000
console.log(calculateBonus('A', 10000));    // 输出:30000

可以看出来实现方式就是,把所有的策略类以函数的形式封装在一个对象里面,然后再通过一个独立的function来充当Context来接收用户的请求,接着将请求委托给适当的策略对象。

6.3 多态在策略模式中的体现

    通过使用策略模式重构代码,我们消除了原程序中大片的条件分支语句。所有跟计算奖金有关的逻辑不再放在 Context 中,而是分布在各个策略对象中。Context 并没有计算奖金的能力,而是把这个职责委托给了某个策略对象。每个策略对象负责的算法已被各自封装在对象内部。当我们对这些策略对象发出“计算奖金”的请求时,它们会返回各自不同的计算结果,这正是对象多 态性的体现,也是“它们可以相互替换”的目的。替换 Context 中当前保存的策略对象,便能执 行不同的算法来得到我们想要的结果。

6.4 策略模式用于表单校验

     在一般情况下我们可能在一些登录、注册页面中进行表单的校验的话,会采用以下的方式:

<html>
<body>
    <form action="http:// xxx.com/register" id="registerForm" method="post"> 请输入用户名:
        <input type="text" name="userName" /> 请输入密码:
        <input type="text" name="password" />请输入手机号码:
        <input type="text" name="phoneNumber" />
        <button>提交</button>
    </form>
    <script>
        var registerForm = document.getElementById('registerForm');
        registerForm.onsubmit = function () {
            if (registerForm.userName.value === '') {
                alert('用户名不能为空');
                return false;
            }
            if (registerForm.password.value.length < 6) {
                alert('密码长度不能少于 6 位');
                return false;
            }
            if (!/(^1[3|5|8][0-9]{9}$)/.test(registerForm.phoneNumber.value)) {
                alert('手机号码格式不正确');
                return false;
            }
        }
    </script>
</body>
</html>

     这是一种很常见的代码编写方式,它的缺点跟计算奖金的最初版本一模一样。存在几个缺点:onsubmit函数体内容庞大,拥有大量if-else语句;函数缺乏弹性,难以修改;复用性差。

使用策略模式重构表单校验
     下面我们将用策略模式来重构表单校验的代码,很显然第一步我们要把这些校验逻辑都封装成策略对象:

var strategies = {
    isNonEmpty: function (value, errorMsg) {
        if (value === '') {
            return errorMsg;
        }
    },
    minLength: function (value, length, errorMsg) {
        if (value.length < length) {
            return errorMsg;
        }
    },
    isMobile: function (value, errorMsg) { // 手机号码格式
        if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
            return errorMsg;
        }
    }
};

然后要实现一个Validator类,这个类在这里作为Context,负责接收用户的请求并且委托给strategy对象。

var Validator = function () {
    this.cache = []; // 保存校验规则
};
Validator.prototype.add = function () {
    var ary = rule.split(':');
    this.cache.push(function () { //
        var strategy = ary.shift();
        ary.unshift(dom.value);
        ary.push(errorMsg); // return strategies[ strategy
    });
};
Validator.prototype.start = function () {
    for (var i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
        var msg = validatorFunc(); // 开始校验,并取得校验后的返回信息 if ( msg ){ // 如果有确切的返回值,说明校验没有通过
        return msg;
    }
}

在页面上使用的时候就要通过下面的方式来使用:

var validator = new Validator(); // 创建一个 validator 对象
/***************添加一些校验规则****************/
validator.add(registerForm.userName, 'isNonEmpty', '用户名不能为空');
validator.add(registerForm.password, 'minLength:6', '密码长度不能少于 6 位');
validator.add(registerForm.phoneNumber, 'isMobile', '手机号码格式不正确');
var errorMsg = validator.start(); // 获得校验结果
return errorMsg; // 返回校验结果

当我们往 validator 对象里添加完一系列的校验规则之后,会调用 validator.start()方法来 启动校验。如果 validator.start()返回了一个确切的 errorMsg 字符串当作返回值,说明该次校验 没有通过,此时需让 registerForm.onsubmit 方法返回 false 来阻止表单的提交。

6.5 给某个文本输入框添加多种校验规则

     目前我们的表单校验实现留有一点小遗憾:一 个文本输入框只能对应一种校验规则,比如,用户名输入框只能校验输入是否为空,如果我们既想校验它是否为空,又想校验它输入文本的长度不小于 10 呢?我们期望以这样的形式进行校验:

validator.add(registerForm.userName, [{
    strategy: 'isNonEmpty',
    errorMsg: '用户名不能为空'
}, {
    strategy: 'minLength:6',
    errorMsg: '用户名长度不能小于 10 位'
}]);

那么我们需要针对validator.add方法进行修改,修改如下:

var Validator = function () {
    this.cache = [];
};
Validator.prototype.add = function (dom, rules) {
    var self = this;
    for (var i = 0, rule; rule = rules[i++];) {
        (function (rule) {
            var strategyAry = rule.strategy.split(':');
            var errorMsg = rule.errorMsg;
            self.cache.push(function () {
                var strategy = strategyAry.shift();
                strategyAry.unshift(dom.value);
                strategyAry.push(errorMsg);
                return strategies[strategy].apply(dom, strategyAry);
            });
        })(rule)
    }
};
Validator.prototype.start = function () {
    for (var i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
        var errorMsg = validatorFunc();
        if (errorMsg) {
            return errorMsg;
        }
    }
};

猜你喜欢

转载自blog.csdn.net/u012863664/article/details/79335209
今日推荐