使用策略模式计算奖金

业务需求:例如,绩效为S的人年终奖有4倍工资,绩效为A的人年终奖有3倍工资,而绩效为B的人年终奖是2倍工资。假设财务部要求我们提供一段代码,来方便他们计算员工的年终奖。

  1. 最初的代码实现
  1. var calculateBonus = function(performanceLevel, salary) {
  2. if (performanceLevel === 'S') {
  3. return salary * 4;
  4. }
  5. if (performanceLevel === 'A') {
  6. return salary * 3;
  7. }
  8. if (performanceLevel === 'B') {
  9. return salary * 2;
  10. }
  11. };
  12. calculateBonus('B', 20000); // 输出:40000 calculateBonus( 'S', 6000 ); // 输出:24000

代码缺点:

  • calculateBonus函数比较庞大,包含了很多if-else语句,这些语句需要覆盖所有的逻辑分支。
  • calculateBonus函数缺乏弹性,如果增加了一种新的绩效等级C,或者想把绩效S的奖金系数改为5,那我们必须深入calculateBonus函数的内部实现,这是违反开放-封闭原则的。
  • 算法的复用性差,如果在程序的其他地方需要重用这些计算奖金的算法呢?我们的选择只有复制和粘贴。
  1. 使用组合函数重构代码
    一般最容易想到的办法就是使用组合函数来重构代码,我们把各种算法封装到一个个的小函数里面,这些小函数有着良好的命名,可以一目了然地知道它对应着哪种算法,它们也可以被复用在程序的其他地方
  1. var performanceS = function (salary) {
  2. return salary * 4;
  3. };
  4. var performanceA = function (salary) {
  5. return salary * 3;
  6. };
  7. var performanceB = function (salary) {
  8. return salary * 2;
  9. };
  10. var calculateBonus = function (performanceLevel, salary) {
  11. if (performanceLevel === 'S') {
  12. return performanceS(salary);
  13. }
  14. if (performanceLevel === 'A') {
  15. return performanceA(salary);
  16. }
  17. if (performanceLevel === 'B') {
  18. return performanceB(salary);
  19. }
  20. };
  21. calculateBonus('A', 10000); // 输出:30000

目前,我们的程序得到了一定的改善,但这种改善非常有限,我们依然没有解决最重要的问题:calculateBonus函数有可能越来越庞大,而且在系统变化的时候缺乏弹性。

  1. 使用策略模式重构代码
    策略模式指的是定义一系列的算法,把它们一个个封装起来。将不变的部分和变化的部分隔开是每个设计模式的主题,策略模式也不例外,策略模式的目的就是将算法的使用与算法的实现分离开来。
    第一个版本是模仿传统面向对象语言中的实现。我们先把每种绩效的计算规则都封装在对应的策略类里面:
  1. // 策略类
  2. var PerformanceS = function () { }
  3. PerformanceS.prototype.calculate = function (salary) {
  4. return salary * 4;
  5. }
  6. var PerformanceA = function () { }
  7. PerformanceA.prototype.calculate = function (salary) {
  8. return salary * 3;
  9. }
  10. var PerformanceB = function () { }
  11. PerformanceB.prototype.calculate = function (salary) {
  12. return salary * 2;
  13. }
  14. // 奖金类
  15. var Bonus = function () {
  16. this.salary = null;
  17. this.strategy = null;
  18. }
  19. Bonus.prototype.setSalary = function (salary) {
  20. this.salary = salary;
  21. }
  22. Bonus.prototype.setStrategy = function (strategy) {
  23. this.strategy = strategy;
  24. }
  25. Bonus.prototype.getBonus = function () {
  26. return this.strategy.calculate(this.salary);
  27. }
  28. // use
  29. var bonus = new Bonus();
  30. bonus.setSalary(1000);
  31. bonus.setStrategy(new PerformanceA());
  32. var result = bonus.getBonus();
  33. console.warn({result}) // {result: 3000}
  34. bonus.setStrategy(new PerformanceB());
  35. var resultB = bonus.getBonus();
  36. console.warn({resultB}) // {result: 2000}

JavaScript版本的策略模式

  1. var strategies = {
  2. 'S': function (salary) {
  3. return salary * 4;
  4. },
  5. 'A': function (salary) {
  6. return salary * 3;
  7. },
  8. 'B': function (salary) {
  9. return salary * 2;
  10. },
  11. }
  12. var calculateBonus = function (salary, level) {
  13. return strategies[level](salary);
  14. }
  15. console.log(calculateBonus(1000, 'S')); // 4000

使用策略模式实现缓动动画

  1. // 动画算法
  2. var tween = {
  3. linear: function (t, b, c, d) {
  4. return c * t / d + b;
  5. },
  6. easeIn: function (t, b, c, d) {
  7. return c * (t /= d) * t + b;
  8. },
  9. strongEaseIn: function (t, b, c, d) {
  10. return c * (t /= d) * t * t * t * t + b;
  11. },
  12. strongEaseOut: function (t, b, c, d) {
  13. return c * ((t = t / d - 1) * t * t * t * t + 1) + b;
  14. },
  15. sineaseIn: function (t, b, c, d) {
  16. return c * (t /= d) * t * t + b;
  17. },
  18. sineaseOut: function (t, b, c, d) {
  19. return c * ((t = t / d - 1) * t * t + 1) + b;
  20. }
  21. };
  22. var Animate = function (dom) {
  23. this.dom = dom; // 进行运动的dom节点
  24. this.startTime = 0; // 动画开始时间
  25. this.startPos = 0; // 动画开始时,dom节点的位置,即dom的初始位置
  26. this.endPos = 0; // 动画结束时,dom节点的位置,即dom的目标位置
  27. this.propertyName = null; // dom节点需要被改变的css属性名
  28. this.easing = null; // 缓动算法
  29. this.duration = null; // 动画持续时间
  30. };
  31. Animate.prototype.start = function (propertyName, endPos, duration, easing) {
  32. this.startTime = +new Date; // 动画启动时间
  33. this.startPos = this.dom.getBoundingClientRect()[propertyName]; // dom节点初始位置
  34. this.propertyName = propertyName; // dom节点需要被改变的CSS属性名
  35. this.endPos = endPos; // dom节点目标位置
  36. this.duration = duration; // 动画持续时间
  37. this.easing = tween[easing]; // 缓动算法
  38. var self = this;
  39. var timeId = setInterval(function () { // 启动定时器,开始执行动画
  40. if (self.step() === false) { // 如果动画已结束,则清除定时器
  41. clearInterval(timeId);
  42. }
  43. }, 19);
  44. };
  45. Animate.prototype.step = function () {
  46. var t = +new Date; // 取得当前时间
  47. if (t >= this.startTime + this.duration ) { // (1)
  48. this.update(this.endPos); // 更新小球的CSS属性值
  49. return false;
  50. }
  51. var pos = this.easing(t - this.startTime, this.startPos,
  52. this.endPos - this.startPos, this.duration);
  53. // pos为小球当前位置
  54. this.update(pos); // 更新小球的CSS属性值
  55. };
  56. Animate.prototype.update = function (pos) {
  57. this.dom.style[this.propertyName] = pos + 'px';
  58. };
  59. var div = document.getElementById('div');
  60. var animate = new Animate(div);
  61. animate.start('left', 500, 1000, 'strongEaseOut');
  62. // animate.start( 'top', 1500, 500, 'strongEaseIn' );

表单校验

  1. // 定义策略对象
  2. var strategies = {
  3. isNonEmpty: function (value, errorMsg) { // 不为空
  4. if (value === '') {
  5. return errorMsg;
  6. }
  7. },
  8. minLength: function (value, length, errorMsg) { // 限制最小长度
  9. if (value.length < length) {
  10. return errorMsg;
  11. }
  12. },
  13. isMobile: function (value, errorMsg) { // 手机号码格式
  14. if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
  15. return errorMsg;
  16. }
  17. }
  18. };
  19. var validataFunc = function () {
  20. var validator = new Validator(); // 创建一个validator对象
  21. /***************添加一些校验规则****************/
  22. validator.add(registerForm.userName, 'isNonEmpty', '用户名不能为空');
  23. validator.add(registerForm.password, 'minLength:6', '密码长度不能少于6位');
  24. validator.add(registerForm.phoneNumber, 'isMobile', '手机号码格式不正确');
  25. var errorMsg = validator.start(); // 获得校验结果
  26. return errorMsg; // 返回校验结果
  27. }
  28. var registerForm = document.getElementById('registerForm');
  29. registerForm.onsubmit = function () {
  30. var errorMsg = validataFunc(); // 如果errorMsg有确切的返回值,说明未通过校验
  31. if (errorMsg) {
  32. alert(errorMsg);
  33. return false; // 阻止表单提交
  34. }
  35. };
  36. var Validator = function () {
  37. this.cache = [];
  38. };
  39. Validator.prototype.add = function (node, rule, errMsg) {
  40. const value = node.value;
  41. const ary = rule.split(':');
  42. this.cache.push(function () {
  43. const strategy = ary.shift()
  44. ary.unshift(value);
  45. ary.push(errMsg)
  46. return strategies[strategy].apply(this, ary);
  47. });
  48. };
  49. Validator.prototype.start = function () {
  50. for (let i = 0, len = this.cache.length; i < len; i++) {
  51. var result = this.cache[i]();
  52. if (result) {
  53. return result;
  54. }
  55. }
  56. };

策略模式的优缺点

优点

  • 策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
  • 策略模式提供了对开放—封闭原则的完美支持,将算法封装在独立的strategy中,使得它们易于切换,易于理解,易于扩展。
  • 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
  • 在策略模式中利用组合和委托来让Context拥有执行算法的能力,这也是继承的一种更轻便的替代方案。

缺点

  • 使用策略模式会在程序中增加许多策略类或者策略对象,但实际上这比把它们负责的逻辑堆砌在Context中要好。
  • 要使用策略模式,必须了解所有的strategy,必须了解各个strategy之间的不同点,这样才能选择一个合适的strategy。