设计模式-提高代码质量

203 阅读6分钟

代码质量 与 设计模式应用

一、代码指标

1、健壮性

什么意思呢? 百度:在异常和危险情况下系统生存的能力。 用人的健壮性做个比方:

如果一个人很健壮,那一般的小病小灾打不倒他,如感冒、咳嗽 对比过来,一个程序很健壮,那在出现预期之外的错误(eg:文件不存在),减少找bug的难度和影响程度。

健壮性好的代码有哪些特点

减少找bug的难度 很多同学可能有这么一个问题:发现自己每天写代码 2小时,改bug 6小时, 一天8个小时就这么过去了 降低对程序的影响程度 如果程序出现了问题,不至于这一个问题让我们整个系统跑不起来了

保证健壮性的手段

健壮性不同其他性能,没有多抽象的概念,也没有多深的思维,主要就是个人的习惯

1、参数层面

  • 基础类型判断

    先来看下面一个例子:

    function add(a, b){
      return a + b;
    }
    // 正常入参 没什么问题
    add(1,2);
    
    // 少传一个参数时
    add(1) // ==> NaN  这里是不会报错的哦!
    
    // 这时如果这个返回值是通过 axios.get()传到后台会是什么情况?
    axios.get('url',{ num: add(1) }) // ==> 400 bad request
    // 试想:如果你的代码量比较大时,要定位到这个问题是不是要花很长时间,还要debug之类的
    
    这就是典型的参数类型引起的问题 [如何解决?]
    function add(a, b){
      // 1、基础参数类型判断
      if(typeof a === 'number' && typeof b === 'number'){
        return a + b;
      } else {
        throw new Error('a or b is not a number')
      }
    }
    

  • 参数是配置对象 选项合并
    // 以Vue为例:
    new Vue({});
    // 有哪些必传参数?
    // 如何保证这些必传参数都传了?
    
    [看Vue是怎么做的?]
    // 必传参数:  el template
    // 如何保证必传值不为空: 提供一个默认配置,把new时的入参与默认配置做合并
    function Vue(config){
      let _default = {
        el: document, 
        template:'<div></div>'
      }
      for(let key in config){
        _default[key] = config[key] || _default[key];
      }
    }
    

  • 参数是某个类的实例

    这种情况,一般是我们在造轮子的时候会用到

    // Class1 是一个类
    function Class1() {
    
    }
    
    // 这里有个方法fn 参数要求是Class1的实例
    function fn(obj){
      
    }
    
    [看这种情况怎么保证健壮性]
    // Class1 是一个类
    function Class1(){
    
    }
    // 这里有个方法fn 参数要求是Class1的实例
    function fn(obj){
      if(obj instanceof Class1){
        // dosomthing
      } else {
        throw new Error('obj 必须是 Class1的实例')
      }
    }
    

这种情况在实际开发中可能用到的比较少, 但在自己造轮子的时候,这种情况就会非常多

  • js中特有的一个问题

    // function 除了可能是一个方法,还可能代表一个什么东东?
    function a(){}
    

    [如何保证?]

    function vue(){
      if(this instanceof vue){ // 通过instanceof vue 判断this 是不是vue实例
        // 类实例
      } else {
        // 当作一个方法调用
        // return new vue(); // 这样即使当一个方法调用 ,拿到的还是一个vue的实例
        
        // 像一些框架之类的都会有这种措施  vue是这样
        throw new Error('vue is a constructor and should be called with the `new` keyword ')
      }
    }
    
    vue(); // 直接调用时,this ---> window
    new vue(); // new 操作符调用时, this 指向vue实例 通过instanceof vue 可以判断
    
    

易错代码

代码错了就是错了,什么叫易错代码?

  • 不由自己控制的出错代码

    // 例如: 前端请求某个接口,接口返回的数据结构
    // 预期结构是下面这样的:
    let res = {
      list:[
        {
          attrA: 1
        }
      ]
    }
    
    // 是这样取值的
    let attrA = res.list[0].attrA;
    
    // 如果 list 为空或[] 时,我们取值就会报错  Cannot read property 'attrA' of undefined
    

    [怎么解决呢?] 一般有两种方法:

    • 模拟一个这样的数据结构
    let item = (res.list || [])[0];
    let attrA = (item || {}).attrA;
    
    • 用&&操作符 先判断为空的情况
    let item = res.list && res.list[0]  // 使用与操作符 判断为空的情况
    let attrA = item && item.attrA;
    

    前端易错性代码出现可能会少些,后端就非常多

    // 在下载某个文件时,
    // 1、文件存在,正常下载
    // 2、文件不存在,就会出错了
    // 如果不处理出错,后端服务可能就停了,那这样影响就非常严重了,
    
    // 所以后端经常会看到这样的代码段
    try{
      // 易错代码
    } catch (e){
      // 记录日志
    }
    
    // 先把出错的信息记录下来,保证程序不会挂掉,
    // 再通过日志分析出错原因
    

变量权限 这个就有点抽象了

变量应该不应该被篡改,能不能够被篡改 变量应不应该被读取,能不能够被读取,在哪里能被读取 这就是变量的权限

变量权限跟健壮性有什么关系呢?

// 还以Vue为例:
// 在使用vue-router的时候,有没有留意这样一个问题:
this.$router = {}; // 给$router赋值,但 this.$router的值不会变

[vue-router是怎么做的呢?]

// vue-router install 方法
function install(Vue){
  let _router = {}; // 局部变量
  // 屏蔽了set方法,使你给$router赋值时无效
  Object.defineProperty(Vue.prototype, '$router', {
    get: function get () { 
      // return this._routerRoot._router // 源码是这样的
      return _router;
    }
  });
}

2、可读性

看到变量或方法就知道这个变量,这段代码是做什么用的

语义化

定义有意义的名字: 描述某个变量或方法在业务中的作用

// 这些都是没有意义, 大量的这种命名,在后期阅读代码时,简直是个灾难
function a(){

}  
function b(){

}

如何命名呢? 可以按这个思路来: 在业务中的作用 --> 用中文描述它的作用 --> 翻译成英文 尽量做到见名知义

命名规范

这个不多说了,

比如:js可以这样命名:
  1、类命名:首字母大写
  2、普通方法变量,小驼峰命名
  3、常量: 全大写,
  4、局部变量:_开头

结构清晰

  • if-else 问题

    if(true){
      if(true){
    
      } else if(true){
    
      } else {
    
      }
    } else if(true){
      if(true){
    
      } else if(true){
    
      } else {
    
      }
    } else {
      if(true){
    
      } else if(true){
    
      } else {
    
      }
    }
    
  • 回调函数问题

    需求:一个操作需要调三个接口A,B,C,B依赖A的返回结果,C依赖B的返回结果

    // 用jquery 实现就是这种样式
    $.ajax({
      url:'urlA',
      params:{},
      success: function(resA){
        $.ajax({
          url:'urlB',
          params: resA,
          success: function(resB){
            $.ajax({
              url:'urlC',
              params: resB,
              success: function(resC){
                console.log(resC)
              }
            })
          }
        })
      }
    })
    

    ES6 之后引用了 promise async 主要就是为了解决回调问题 避免回调地狱

    // promise then 方式解决
    function request(url, params){
      return new Promise((resolve,reject) => {
        $.ajax({
          url:url,
          params: params,
          success:function(res){
            resolve(res);
          },
          error: function(err){
            reject(err)
          }
        })
      })
    }
    // 调用
    request('urlA', {})
      .then(res => request('urlB', res))
      .then(res => request('urlC', res))
      .catch(err => console.log(err));
    
    // async
    async function getData(url, params){
      return axios.post(url, params);
    }
    //调用
    let resA = await getData('urlA', {});
    let resB = await getData('urlB', resA);
    let resC = await getData('urlC', resB);
    

3、可复用性

DRY原则 Don't Repeat Yourself

写过一遍的操作,就不重复第二遍了 尽量不去写重复代码, 这可能带来一些维护上的负作用,这个就要把握一个度了 要看某段代码有没有提取的必要,比如重复了多少次,使用量大不大

逻辑复用,提取代码

针对局部某个操作,某个功能而言 重复的部分提取成一个公用的方法

创建公用模板

针对全局性的,创建公用模块: layout | header | footer | common.css | util.js

4、可扩展性

程序上的一个终极难题 对开发人员的架构思维,模块思维要求是非常高的

产品经理不可能不修改他的需求,就像程序员不可能不写bug一样 如果在写代码的时候预先考虑到后期需求可能的变化,那在需求变化时,你就会非常舒服了

  • 模块分明

    积木式编程,随时可以插入、移除某个模块

  • 耦合度低

    划分低耦合的模块,并高效设计模块间的沟通 (架构层面讲) 比如你要开个饭店,你需要怎么设置你的组织架构?

    厨师模块,服务员模块,收银员模块(老板)

  • 合适的扩展技巧

    应用设计模式

二、设计模式概论

1、创建型设计模式

帮助我们优雅的创建对象

  • 工厂模式

    大量创建对象

    Jquery时代,我们需要大量频繁的操作dom

    // $就是一个工厂,他批量生产jquery对象, 根据你传入的选择器,生成一批jquery对象
    $('.className')
    

    [如何实现一个工厂模式呢?] 球类工厂

    需求:我们要生产不同类型的球:basketball footerball tennis(网球)...

    // 球类工厂
    function ballFactory(type){
    
      switch(type){
        case 'football':
          return new football();
          break;
        case 'basketball':
          return new basketball();
          break;
        case 'tennis':
          return new tennis();
          break;
        default:
          break;
      }
      
      // 可以在局部 
      function football(){} 
      function basketball(){}
      function tennis(){}
    }
    
    // 可以在原型链上
    ball.prototype.football = function(){}
    ball.prototype.basketball = function(){}
    ball.prototype.tennis = function(){}
    
    

    [再看看jquery是如何实现的] Jquery如何实现工厂模式的

    // 静态函数 jquery的实现
    function jquery(params){
      return new jquery.fn.init(params);
    }
    jquery.fn = {};
    jquery.fn.init = function (params) {};
    window.$ = jquery;
    
    $('.className');// 实际是通过new jquery静态属性fn上的init方法
    

    jquery是通过挂载一个静态属性实现的


  • 建造者模式

    精细化组合一个对象:类似建房子 盖房子明, 先把砖,门,窗这些材料都准备好,再把这些材料搭在一起,就建成了.

    关键思想 就是先把相关的模块提前独立开发好,再把各个模块拼装集成到一起

    明显的标识就是 传入了一大堆的配置信息

    //建造者模式: 一般实现方案
    
    // 关键思想 先把相关的模块放一边开发好,再把各模块集成到一起
    function F1(){
      // 直接绑定
      this.model1 = new Model1({});
      this.model2 = new Model2({});
      this.model3 = new Model3({});
    }
    
    function Model1(params){}
    function Model2(params){}
    function Model3(params){}
    

    vue 也是用建造者模式实现的, 但双是跟一般的方案又不太一样 混入方式

    // 明显的标识就是 传入了一大堆的配置信息
    // 方法2 Vue 是怎么做的 
    // 先把各个模块独立开发好,之后再混合进实例里
    function Vue (options) {
      // 健壮性校验
      if (!(this instanceof Vue)
      ) {
        warn('Vue is a constructor and should be called with the `new` keyword');
      }
      this._init(options);
    }
    
    // 下面就是各个模块的混入Vue原型上
    initMixin(Vue);
    stateMixin(Vue);
    eventsMixin(Vue);
    lifecycleMixin(Vue);
    renderMixin(Vue);
    
  • 单例模式

    全局只有一个,对象只能被创建一次

    如何实现 单例

    // 常规类:靠挂在这个类上面的一个静态属性
    function Class1(params){
      if(Class1.instance){ // 查检有没有实现,有则直接返回该实例,无则创建实例,并增加标识
        return Class1.instance;
      } else {
        this.name ='xxx'
        this.attr = 'bbbb';
        
        Class1.instance = this;
      }
    }
    // 两个new 是同一个实例
    let class11 = new Class1();
    let class12 = new Class1();
    console.log(class11 == class12)
    

    再看vuex vue-router vuex: 全局只有一个,如果有多个,那页面的状态从哪个取也是问题 vue-router:全局中有一个,如果有多个的话,页面跳转的时候,选用哪个就是一个问题

    // 以vue-router为例子
    // vue插件时如何保证单例的 以vue-router为例
    // 通过一个参数,变量,静态属性去标识这个类是否被new 过,已经new过的,不再去new
    Vue.use(vueRouter); 
    // Vue.use 实际上是调用了插件的install方法
    
    // vue-router
    {
      var _vue; 
      function install(vue){
        if(_vue === vue && install.installed) 
          return;
        _vue = vue;
        install.installed = true;// 静态属性
      }
    }
    

问题:

需求1:实现一个消息提示弹框插件,[用什么模式?] 弹框提示插件 分析: 因为一个页面的消息提示可能会很多,添加成功,添加失败,网络出错 会需要频繁创建对象,所以考虑用工厂模式实现

// 工厂模式实现 调用如下
pop('message')
// OR
pop.confirm('message')

// 建造者模式实现,调用如下
pop('message')
// OR
let popObj = new pop('message')
popObj.show();

// 哪个用起来更方便, 虽然看起来只少了一个new

需求2: 实现一个编辑器插件:有前进,后退,改变字体,大小,颜色功能,[用什么模式?] 编辑器插件 需求: 有前进,后退,改变字体,大小,颜色功能

分析: 1、一般一个页面只有一个,不需要频繁创建 2、编辑器可能还要涉及到复杂的配置,需要精细化创建

建造者模式

// html 初始化--> 事件绑定 --> 前进后退模块 -->数据记录模块 --> 字体控制模块 --> 数据渲染模块
// 前进后退: 一般都是数据驱动思维,
[{color:'red', content:'hello'}, {color:'green', content:'hello'}]

2、结构型设计模式

帮助我们更优雅的设计代码结构: 策略,享元

还以一个具体的需求为例:

需求:写一个表单验证工具,给我要验证的input 值 value 变化时,应用对应的规则,自动验证 分析: 1、首先是包含的模块:dom初始化模块 --> 事件绑定模块 --> 验证模块 --> 消息提示模块 2、一个页面上可能有多个需要验证的dom,所以适合工厂模式

// 1、防JQuery实现

// 初始化
function t(dom, msgDom){
  return new t.init(dom, msgDom);
}
t.init = function(dom, msgDom){
  this.dom = dom;
  this.msgDom = msgDom;
  this.validateArr = [];
}
// 事件绑定
t.init.prototype.initBind = function(params){
  let self = this;
  this.dom.onblur = function(){
    self.run(this.value)
  }
}
// 验证模块 
// 变化可能性最大,需要细化
// 考虑验证模块在以后的需求中可能需要扩展,
// 所以可以提前预留好自定义扩展方法,预置一些基础的验证规则,减少代码重复
// 以队列的形式存放验证规则,方便扩展
dt.init.prototype.add = function(fn){
  if(typeof fn === 'function'){
    this.validateArr.push(fn);
  } else if(typeof fn === 'string'){
    // 预置的验证规则 下面这样写,可读性差,代码不清楚
    if(fn === 'isPhone'){
      this.validateArr.push(()=>{/*手机号验证*/})
    } else if(fn === 'isNumber'){
      this.validateArr.push(()=>{/**是否是数字*/}) 
    } else if(fn === 'isEmail'){

    }
  }
}
t.init.prototype.run = function(value){
  while(this.validateArr.length > 0){
    // _result 是约定好的验证结果的数据结构
    // {success: true|false, msg:''}
    let _result = this.validateArr.shift().run(value);
    if(!_result.success){
      this.sendMsg(_result.msg);
      break; // 一个验证失败就停止验证,减少不必要的循环
    }
  }
}
// 消息提示模块
t.init.prototype.sendMsg = function(msg){
  this.msgDom.innerHtml = msg;
}


// 后期的使用   职责链模式
t('input', 'errorMsg')
  .add('isPhone')
  .add(() => {/**自定义验证1 */})
  .add(() => {/**自定义验证2 */})

[优化后的验证工具] 表单验证工具

需求:写一个表单验证工具,给我要验证的input 值 value 变化时,应用对应的规则,自动验证 分析: 1、首先是包含的模块:dom初始化模块 --> 事件绑定模块 --> 验证模块 --> 消息提示模块 2、一个页面上可能有多个需要验证的dom,所以适合工厂模式

// 1、防JQuery
function t(dom, msgDom){
  return new t.init(dom, msgDom);
}
t.init = function(dom, msgDom){
  this.dom = dom;
  this.msgDom = msgDom;
  this.validateArr = [];
}
// 2、思考以后的扩展性,想想模块是否需要细化, 模块越细,扩展越方便
// 验证模块变化最大,细化
// 开启验证模块,验证队列模块
t.init.prototype.initBind = function(params){
  let self = this;
  this.dom.onblur = function(){
    self.run(this.value)
  }
}

t.init.prototype.add = function(fn){
  if(typeof fn === 'function'){
    this.validateArr.push(fn);
  } else if(typeof fn === 'string'){
    // 预置的验证规则 下面这样写,可读性差,代码不清楚
    // 引入设计模式解决
    // if(fn === 'isPhone'){
    //   this.validateArr.push(()=>{/*手机号验证*/})
    // } else if(fn === 'isNumber'){
    //   this.validateArr.push(()=>{/**是否是数字*/}) 
    // }

    // 策略模式解决 
    // 简单if-else可以很好解决
    let strage = {
      isPhone:function(params){},
      isNumber: function(params){},
      // 更多验证规则
    }
    this.validateArr.push(strage[fn]);

    // 这里涉及的逻辑比较简单,如果逻辑更复杂一点,可能这一个简单的策略就无法解决
    // 比如:由单一的条件变化,变成几个组合条件变化

    // 状态模式
    // 核心:根据对象不同的状态,让对象展示不同的行为, 相当于加了状态管理的策略模式
  }
}

t.init.prototype.run = function(value){
  while(this.validateArr.length > 0){
    // _result 是约定好的验证结果的数据结构
    // {success: true|false, msg:''}
    let _result = this.validateArr.shift().run(value);
    if(!_result.success){
      this.sendMsg(_result.msg);
      break;
    }
  }
}

t.init.prototype.sendMsg = function(msg){
  this.msgDom.innerHtml = msg;
}

// 后期的使用   职责链模式
t('input', 'errorMsg')
  .add('isPhone')
  .add(() => {/**自定义验证1 */})
  .add(() => {/**自定义验证2 */})

需求2:一个div 实现 上、下、左、右、左上、左下、....这样移动 moveDiv('left') // 左移 moveDiv('left','top') // 左上移

function moveDiv(){
  if(arguments.length === 1){
    // 可以用策略模式
    if(arguments[0] == 'left'){
      moveLeft();
    } else if (arguments[0] === 'right'){
      moveRight();
    }
  } else if(arguments.length === 2){
    // 这里简单的策略模式就不容易了
    if(arguments[0] == 'left' && arguments[1] == 'top'){
      moveLeft();
      moveTop();
    }
  }
}

[状态模式实现moveDiv] 移动Div

需求2:一个div 实现 上、下、左、右、左上、左下、....这样移动 moveDiv('left') // 左移 moveDiv('left','top') // 左上移

// 用状态模式实现
// 这时候moveDiv就成了一个类了,不是一个方法了
function moveDiv(params){
  this.stateArr = [];//因为存在复合运动的行为,所以需要一个数组去存储数据
}
moveDiv.prototype.run = function(params){
  // arguments 类数组  将类数组转成真正数组
  // 这里有几种方法把类数组转成数组?
  this.stateArr = Array.prototype.slice.call(arguments);
  // 策略模式
  let strage = {
    left: moveLeft,
    right:moveRight,
    top:moveTop
  };
  this.stateArr.forEach(state => {
    strage[state]();
  });
}
let moveObj = new moveDiv();
moveObj.run('left', 'top');

使用设计模式之后,这个模块的代码更简洁更易读了
享元模式

存在类似对象和类似代码块时,用于减少类似代码块 这个享元是提取 相同的内容,还是不同的内容? 享元模式里的享无是 代码中的不同点 把不同的提出来,剩下的就是相同的,这样代码块就由多化一了 与平时提取公共代码不是一个意思

以jquery.extend方法为例 extend实现的功能: $.extend({a:1}) // 会把对象扩展到$对象上$.a = 1; 为jquery对象扩展方法属性使用 $.extend({a:1},{b:2}) // ==> {a:1,b:2}

// 不用设计模式时,是这样实现的
$.extend = function(){
  if(arguments.length === 1){
    for(let item in arguments[0]){
      this[item] = arguments[0][item]
    }
  } else if(arguments.length === 2){
    for(let item in arguments[1]){
      arguments[0][item] = arguments[1][item];
    }
  }
}

[jquery的extend 享元模式实现] jquery.extend 享元模式实现

extend实现的功能: $.extend({a:1}) // 会把对象扩展到$对象上$.a = 1; 为jquery对象扩展方法属性使用 $.extend({a:1},{b:2}) // ==> {a:1,b:2}

// 不用设计模式时,是这样实现的
$.extend = function(){
  // 享元模式,提取不同点:
  // 1、for in 的对象不同,
  // 2、接收的对象不同
  let source = arguments[0];
  let target = this;
  if(arguments.length === 2){
    source = arguments[1];
    target = arguments[0];
  }
  for(let item in source){
    target[item] = source[item]
  }

}

总结应用场景: 两个if else 分支中,两段代码块非常相似时,就可以用享元模式了

3、行为型设计模式

模块间的行为模式的总结,帮助组织模块的沟通:装饰者 观察者

装饰者模式

目的:不重写方法的扩展方法 应用场景:当一个方法需要扩展,但又不方便或不能去修改方法时 是公共的方法 或他人的方法 或原生方法 或第三方模块的方法 简单理解 在不去修改原方法的情况下,扩展方法的功能

需求: 删除按钮--> 点击可以删除 ---> 但没有提示 ---> 好多删除按钮都是这样实现,产品需求是要给出删除提示 分析:在原来的删除功能基础上,扩展出提示功能

1、全部改写 2、找到原来定义,修改原方法,增加提示

[你会选哪种方式实现?] 装饰者模式

目的:不重写方法的扩展方法 应用场景:当一个方法需要扩展,但又不方便或不能去修改方法时 是公共的方法 或他人的方法 或原生方法 或第三方模块的方法 简单理解 在不去修改原方法的情况下,扩展方法的功能

需求: 删除按钮--> 点击可以删除 ---> 但没有提示 ---> 好多删除按钮都是这样实现,产品需求是要给出删除提示 分析:在原来的删除功能基础上,扩展出提示功能

1、全部改写 2、找到原来定义,修改原方法,增加提示

function decorate(dom, fn){
  // 健壮性校验
  if(typeof dom.onClick === 'function'){
    // 装饰者 三步走
    // 1、重写原方法,或定义新方法;
    // 2、提取老方法,并调用
    // 3、加入新方法

    let _oldFn = dom.onClick; // 在方法被重写前提取
    dom.onClick = function(params){
      _oldFn.call(this);
      fn.call(this);
    }
  }
}
// 使用
decorate('btnDel', function(params){
  console.log('删除成功')
});

vue2 的双向数据绑定

// Vue的双向绑定  非数组属性
Object.defineProperty(vue,'dataKey',{
  get: function(){ },
  set: function(){ }
})
// 是对象里的属性变了,会触发, 它没办法用到数组上,那Vue对数组是怎么实现双向绑定的

[vue2 对数组属性实现双向绑定] Vue2 数组的双向绑定


// 数组:利用装饰者模式,给数组的 push pop 
let arr = ['push', 'pop', 'shift']
let arrProto = Array.prototype;
let arrCopy = Object.create(arrProto); // 为了不污染旧原型链,提前拷贝一份出来
arr.forEach(method => {
  arrCopy[method] = function (){
    let _ref = arrProto[method].apply(this, arguments);
    dep.notify();// 触发更新
  }
})
// 把data里的所有数组,都变成这里的这个arrCopy
// 主要是一些技巧

**观察者模式** > 应用场景: > 1、异步模块沟通 >>a 异步模块;b 同步模块; b需要a处理完成后a的消息 观察者 类似事件监听

2、方便加入新的模块, 本来没有想到某个模块会突然要加入的情况

例如:聊天室的沟通:我和你在聊天室的沟通(聊天室就是观察者),这样别人加入更容易 根本没有考虑过你要加入时,你非要加入,实在没办法了,才会引入观察者模式; 虽然使用观察者容易做到某件事儿,但它也确实要花费一些开销

看这样一个抽奖的需求

需求:抽奖转盘,特点是越转越快, 模块分析: 奖品初始化html --> 最终结果选定 --> 转动控制 --> 转动效果

转动控制模块 调用 转动效果模块, 转动效果只负责转动效果; 转完之后,询问控制模块接下来怎么转; 转动效果模块接收消息体{moveTime: 10, speed: 200} 多长时间内转动几个奖品 --> setInterval 异步了, 转动控制与转动效果的沟通问题在于:转动控制不知道什么时候转动效果结束,因为转动速度是不恒定的

// 先定义好几个模块

// 初始化模块
function htmlInit(){ }

// 抽奖结果模块
function getResult(){ }

// 转动控制模块
function moveControll(){ }

// 动画效果:1,2,3,4,5...10 依次高亮显示
function move(config){
  let timer = setInterval(() => {
      // 转完一个周期后需要向控制块请求接下来怎么转
      // 不用设计模式时,就直接通过callback访问
      // 要使用设计模式的话,应该用什么模式?
  }, config.speed);
}

[抽奖转盘 观察者模式] 观察者模式

需求:抽奖转盘,特点是越转越快, 模块分析: 奖品初始化html --> 最终结果选定 --> 转动控制 --> 转动效果

转动控制模块 调用 转动效果模块, 转动效果只负责转动效果; 转完之后,询问控制模块接下来怎么转; 转动效果模块接收消息体{moveTime: 10, speed: 200} 多长时间内转动几个奖品 --> setInterval 异步了, 转动控制与转动效果的沟通问题在于:转动控制不知道什么时候转动效果结束,因为转动速度是不恒定的

// 观察者三要素:队列,注册,触发
function observer(params){
  // 1、队列
  this.message = {

  }
}
// 2、注册
observer.prototype.regist = function(type, fn){
  this.message[key] = fn;
}
// 3、触发
observer.prototype.fire = function(type){
  this.message[key]();
}

var observerObj = new observer();

var _domArr = [];
function htmlInt(params){
  for(let i = 1; i <= 5; i++){
    _domArr.push(document.body.append(`<div>${i}</div>`));
  }
}
function getResult(params){
  let _num = Math.random() * 10 + 40; // 40是基础动画圈数
  return Math.floor(_num, 0);
}
function moveControll(params){
  let result = getResult();
  let _circle = Math.floor(result/10, 0);// 基础动画圈
  let _runCircle = 0; // 已经转了多少圈
  let stopNum = result%10; // 最终停留的奖品数
  let _speed = 200; // 转速
  observerObj.regist('finish', () => {
    let _time = 0; // 应该转几个数
    _speed -=50; // 转一圈 速度加快50
    _runCircle++; // 已转的圈数
    if(_runCircle <= _circle){ // 未达到指定的圈数
      _time = 10; // 继续转10个数
    } else {
      _time = stopNum;
    }

    move({moveTime: _time, speed: _speed});
  });
}
// 动画效果:1,2,3,4,5...10 依次高亮显示
function move(config){
  let nowIn = 0;
  let rmNum = 9; // 移除效果的索引
  let timer = setInterval(() => {
    // 单独处理第10个跳第1个的情况
    if(nowIn == 0){  // 代码相似了,可以优化了
      _domArr[9].setAttribute('class', 'item');
      _domArr[nowIn].setAttribute('class', 'item item-on');
    } else{
      _domArr[nowIn-1].setAttribute('class', 'item');
      _domArr[nowIn].setAttribute('class', 'item item-on');
    }
    
    // 享元模式 
    // if(nowIn != 9){
    //   rmNum = nowIn--
    // }
    // _domArr[rmNum].setAttribute('class', 'item');
    // _domArr[nowIn].setAttribute('class', 'item item-on');

    nowIn++;
    if(nowIn == config.moveTime){
      clearInterval(timer);
      observerObj.fire('finish');
    }
  }, config.speed);
}

创建型设计模式 --> 创建对象阶段使用 结构型设计模式 --> 用在一个对象内部代码优化 行为型设计模式 --> 模块之间的沟通交互

三、设计模式解决问题

  • 1、if-else模式
  • 2、减少重复代码
  • 3、更好的扩展方法
  • 4、解耦模块

原文地址:yolkpie.net/2020/12/14/…