前端-设计模式-读书总结

120 阅读34分钟

前言

设计模式定义:面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。

通俗来说,就是可重复使用的特定场景的特定解决方案

js面向对象三大特性

封装,继承,多态

js同时还是动态类型语言

动态类型:运行时,待变量被赋值后,才具备某种类型

静态类型:编译阶段,已经确认了变量的类型

鸭子类型(duck typing)

如果它走起路来像鸭子,叫起来也是鸭子,那么它就鸭子。

多态(polymorphism)

意思:多种对象,多种状态,也就是多态性 多态的作用:通过把状态赋予对象,消除操作上特定的条件分支语句,使得操作更加简洁优雅

    //实例代码
    // 一个方法makeSound
    const makeSound = function(animal){
        if(animal instanceof Duck){
            console.log('嘎嘎嘎')
        }
        if(animal instanceof Mouse){
        console.log('吱吱吱')
        }
    }
    function Duck(){}
    function Mouse(){}
    makeSound(new Duck()) // '嘎嘎嘎'
    makeSound(new Mouse()) // '吱吱吱



    const makeSound = function(animal){
        if (animal.sound) {
          animal.sound()
        }else{
          console.log('---');
        }
    }
    function Duck(){}
    Duck.prototype.sound = ()=>console.log('嘎嘎嘎')
    function Mouse(){}
    Mouse.prototype.sound = ()=>console.log('吱吱吱')
    makeSound(new Duck()) // '嘎嘎嘎'
    makeSound(new Mouse()) // '吱吱吱

封装

封装包括:封装数据,封装实现,封装类型,封装变化

封装的最终目的是:隐藏-隐藏对象类型-隐藏实现细节-隐藏设计细节等

封装实现

对象:对象对自己的行为负责,其他对象或者用户不需要关心它的内部,对象之间只通过暴露的API接口来通信。当我们修改一个对象时,随意修改,只要对外接口没有变化,就不会影响程序的其他功能。对象之间的耦合变的松散

封装类型

通过抽象的类和接口,把对象真正类型隐藏再抽象类和接口中,同时把客户更关心的对象的行为暴露出去

(主要是ts实现,如装饰器)

封装变化

封装在设计模式中,重心是体现为封装变化

封装变化的思维:怎么样才能在不重新设计的情况下进行变化

找到这些特定问题或场景的变化,将它封装起来,同时也体现了可复用

继承

就是存在在父对象的前提下,生成的子对象会拥有父对象的所有属性和方法,也拥有直接的属性和方法

javascript设计之初,是不存在类的概念,所以不能像c++/java这些语言一样,可以通过类创造和实现直接继承

所以诞生出,原型模式,组合模式等方式实现js的继承

1.原型模式

原型模式是创建对象的一种模式。

如果我们想要创建一个对象,一种方法时先指定它的类型,然后通过这类创建这个对象。

原型模式选择了另一种方式,我们不再关心对象的具体类型,而是找到一个对象,通过克隆来创建一个一模一样的对象

原型模式实现的关键是:语言本身是否提供了clone方法,es5提供了Object.create方法可以用来克隆对象

原型模式的基本规则:

  1. 所有的数据都是对象
  2. 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它
    • 对象会记住他的原型
    • 如果对象无法响应某个请求,它会把这个请求委托给它自己的原型
    function Plane() {
      this.blood = 100
      this.attackLevel = 1
      this.defenseLevel = 1
    }
    var plane = new Plane()
    plane.blood = 500
    plane.attackLevel = 3
    plane.defenseLevel = 3
    var clonePlane = Object.create(plane)
    console.log(clonePlane.blood);// 找到clonePlane的原型并把获取blood属性的请求委托给它自己的原型

	//create实现原理,是通过临时创建一个Fn,将Fn的prototype属性指向克隆对象本身。然后返回 new Fn()
	// 下面是简单实现
    function myCreate(obj) {
      function fn() {}
      fn.prototype = obj
      return new fn()
    }

高阶函数

  • 函数作为参赛传入
  • 函数作为返回值输出

AOP面向切面编程

AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。这样做的好处首先是可以保持业务逻辑模块的纯净和高内聚性,其次是可以很方便地复用日志统计等功能模块

函数柯里化Currying

一个currying函数首先会接收一些参数,接收了这些残守之后,该函数并不会立即求职,而是继续返回另一个函数,刚才传入的参数在函数形成闭包中被保存起来。待到函数被真正需要求值时,之前传入的所有参数会被一次性用于求值

函数节流

函数被频繁触发,而我们不需要它那么高频的触发,所以我们要使它节流,就是把触发频率变低,变为我们规定的一定时间内触发一次。

  window.onload = function () {
    let throttle = function (fn, interval = 500) {
      let selt = fn // 存储函数
      let isFirst = true //是否为第一次触发
      let timer = null
      return function (...args) {
        let context = this
        if (isFirst) {
          fn.apply(context, args)
          isFirst = false
          return
        }
        if (timer) {
          return
        } else {
          timer = setTimeout(() => {
            fn.apply(context, args)
            clearTimeout(timer)
            timer = null
          }, interval);
        }
      }
    }
    function onsizeFn() {
      console.log(this, 'onsizeFn');
    }
    function ThrottleOnsizeFn() {
      console.log(this, 'ThrottleOnsizeFn');
    }
    let mydiv = document.getElementById('mydiv')
    for (let index = 0; index < 100; index++) {
      let div = document.createElement('div')
      div.innerHTML = `这是第${index}个div`
      mydiv.appendChild(div)
    }
    window.addEventListener('resize', onsizeFn)
    window.addEventListener('resize', throttle(ThrottleOnsizeFn))
  }

2.单例模式

理解:

使用一个变量来标记当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象

透明的单例模式

  var creatDiv = (
    function () {
      var instance ;
      var creatDiv = function (html) {
        if (instance) {
          return instance
        }
        this.html = html
        return instance = this
      }
      return creatDiv
    }
  )()
  var a = new creatDiv('span1')
  var b = new creatDiv('span2')
  console.log(a===b);

利用闭包把首次创建的实例instance保存起来。再利用new 操作符的作用把instance指向首次创建的对象实例指向。

缺点:每次创建一个单例都需要修改其中的createDiv函数。这就与业务耦合一起了

用代理实现单例模式

  // 单例代理器
  var proxySingleton = function (classFn) {
    var instance;
    return function () {
      if (!instance) {
        instance = new classFn(...arguments);
      }
      return instance;
    };
  };
  // 业务函数
  function animal(name) {
    this.name = name;
  }
  // 创建单例函数
  var singletonAnimal = proxySingleton(animal);
  var a = new singletonAnimal('牛')
  var b = new singletonAnimal('牛')
  console.log(a,a===b);

使用单例代理器生成和业务函数,生成一个业务的单例函数。实现与业务的解耦

惰性单例

需要的时候才创建对象实例,称为惰性单例

  // 惰性单例
  var getSingle = function (classFn) {
    var result;
    return function () {
      return result || (result = classFn.apply(this, arguments));
    };
  };
  // 业务函数
  function animal(name) {
    this.name = name;
  }
  function robot(name) {
    this.name = name;
  }
  // 创建单例函数
  var singletonAnimal = getSingle(animal);
  var singletonRobot = getSingle(robot);
  var a1 = new singletonAnimal("牛");
  var b1 = new singletonAnimal("牛");
  var a2 = new singletonAnimal("机器牛");
  var b2 = new singletonAnimal("机器牛");
  console.log(a1, a1 === b1);
  console.log(a2, a2 === b2);

3.策略模式

定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。将算法的使用和算法的实现分离开来。

一个基于策略模式的程序至少由两部分组成。第一部分是一组策略类,策略类封装了具体的算法,并且负责具体的计算过程。第二个部分是环境类Context,Context接收客户的请求,随后把请求委托给某个策略类。要做到这点,Context中要维持某个策略对象的引用。

面向对象的策略模式

  // 一个按评级,发放不同奖金的业务
  var calculateBonus = function (performanceLevel, salary) {
    if (performanceLevel === "A") {
      return salary * 4;
    }
    if (performanceLevel === "B") {
      return salary * 3;
    }
    if (performanceLevel === "C") {
      return salary * 2;
    }
    if (performanceLevel === "D") {
      return salary * 1;
    }
  };
  // 以后有更多的A+,A-这些级别的业务需求时。我们需要不断的修改这个calculateBonus函数。这将违背了我们的放封闭原则

  // 使用策略模式-1,封装策略类
  class performanceA {
    calculate(salary) {
      return salary * 4;
    }
  }
  class performanceB {
    calculate(salary) {
      return salary * 3;
    }
  }
  class performanceC {
    calculate(salary) {
      return salary * 2;
    }
  }
  class performanceD {
    calculate(salary) {
      return salary * 1;
    }
  }

  // 定义奖金类Bonus
  class Bonus {
    constructor() {
      this.salary = null;
      this.strategy = null;
    }
    setSalary(salary) {
      this.salary = salary;
    }
    setStrategy(strategy) {
      this.strategy = strategy;
    }
    getBonus() {
      return this.strategy.calculate(this.salary);
    }
  }

  // 使用
  var bonus = new Bonus();
  bonus.setStrategy(new performanceA());
  bonus.setSalary(4000);
  console.log(bonus.getBonus());

定义一系列的算法,把它们各自封装成策略类performanceA,算法被封装再策略类内部的方法里calculate。在客户对context(一个new Bonus)发起请求时,context总是把这个请求委托给这些策略对象中间的某一个进行计算 performance类中的calculate函数

javacsript终端策略模式

  var strategies = {
    A: function (salary) {
      return salary * 4;
    },
    B: function (salary) {
      return salary * 3;
    },
    C: function (salary) {
      return salary * 2;
    },
    D: function (salary) {
      return salary * 1;
    },
  };
  function calculateBonus(performanceLevel, salary) {
    return strategies[performanceLevel](salary);
  }
  console.log(calculateBonus("A",200));

这种对象函数的形式使得策略模式在JavaScript中体现的更为简洁。

策略模式,总的说也是为了取消程序中的if分支,使得策略可以复用替换,而context更为关注自身的私有属性。

策略模式的优缺点

优点:

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

缺点:

  • 策略模式增加了需要策略类跟策略对象
  • 使用策略模式必须了解所以的strategy,懂得其中的不同点,可以复用点

4.代理模式

当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理后,再把请求转交给本体对象

保护代理和虚拟代理

保护代理:用于控制不同权限对象对目标对象的访问。

虚拟代理:提供一个虚拟对象,节省程序的开销,等真正需要执行时,虚拟代理再把这些操作提供给真实对象

虚拟代理实现突破预加载

  // 创建一个真实的img
  var myImage = (function () {
    var imgNode = document.createElement("img");
    document.body.appendChild(imgNode);
    return {
      setSrc: function (src) {
        imgNode.src = src;
      },
    };
  })();
  // 使用虚拟代理
  var proxyImage= (function () {
    var img =  document.createElement("img");
    var src ;
    img.onload = function(){
      // 虚拟对象先假装src图片。在加载完成后,修改正真图片元素的src,由于网络缓存,不需要重复的网络开销
      myImage.src =  this.src
    }
    return {
      setSrc:function(src){
        myImage.src('这是一张loading图片!.png')
        img.src = src
      }
    }
  })()
  proxyImage.setSrc('我要展示这个图片.png')

单一职责原则

就一个类(包括对象和函数等)而言,应该仅有一个引起它变化的原因。如果一个对象程度了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。

面向对象设计鼓励将行为分布到细粒度的对象之中,如果一个对象程度的职责过多,等于吧这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计,当变化发生是,设计肯会遭遇意外的破坏。

缓存代理

可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果

缓存代理乘积函数

  // 乘积函数
  function mult() {
    console.log("开始乘积运算");
    var a = 1;
    for (let index = 0; index < arguments.length; index++) {
      const element = arguments[index];
      a = a * element;
    }
    return a;
  }
  // 使用闭包对乘积函数进行缓存代理
  var proxyMult = (function () {
    var cache = {};
    return function () {
      var args = Array.prototype.join.apply(arguments);
      if (args in cache) {
        return cache[args];
      }
      return (cache[args] = mult.apply(this, arguments));
    };
  })();
  console.log(proxyMult(1,2,3,4,5));
  console.log(proxyMult(1,2,3,4,5));

缓存代理工厂

var createProxyFactory = function(fn){
  var cache = {}
  return function(){
    var args = Array.prototype.join.apply(arguments)
    if(args in cahe){
      return cache[args]
    }
    return (cache[args] = fn.apply(this,arguments))
  }
}
var proxyMult = createProxyFactory(mult)

总结

代理分为很多种,而javascript开发中最常用的是虚拟代理和缓存代理。

虽然这些代理模式非常有用,但我们写业务代码时,往往不需要预先猜测是否需要使用代理模式。当正真发现不方便直接访问某个对象时,再编写代理也是可行的。

5.迭代器模式

是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素

JavaScript中的forEachmap、、、等

内部迭代器和外部迭代器

内部迭代器:

内部已经定义好了迭代规则,它可以完全接受整个迭代过程,外部只需要一次初始化抵用(如foreach)

外部迭代器:

必须显示的请求迭代下一个元素。外部迭代器增加了一调用的复杂度,但相对也增强了迭代器的灵活性,我们可以手工控制迭代的过程或者顺序。(如一些tree的迭代)

总结

迭代器还有,逆序迭代,中止迭代等等,同时可以使用迭代器模式去取代一些if分支语句,抽离耦合的函数。目前对大部分语言内置了迭代器,虽然它简单的不让我们认为是一种设计模式,但是它是实现业务必不可少的理解。

6.发布-订阅模式(观察者模式)

发布-订阅模式又称为观察者模式,它定义对象之间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

发布-订阅模式广泛应用于异步编程中,这是一种替代传递回调函数的方案。如:订阅ajax请求的error、succ等事件,在异步编程中,我们不需要过多的关注对象再异步运行期间的内部状态,而只需要订阅感兴趣的事件发生点。

发布-订阅模式可以取代对象之间硬编码的通知机制,一个对象不用再显示地调用另外一个对象的某个接口。发布-订阅模式让两个对象松耦合的联系再一起。虽然不太清楚批次的细节,但这不影响它们之间相互通信。当有新的订阅者出现时,发布者的代码不需要任何修改;同样发布者需要改变是,也不影响之前的订阅者。只要之前约定的事件名没有变化,就可以自由地改变它们。

基本效果实例

  var EventModel = (function () {
    //...
  })();

  // 发布-订阅者模式除了普通的订阅发布,
  // 还能实现--先发布-后订阅
  EventModel.trigger("click", 1);
  EventModel.listen("click", function (val) {
    console.log(val); // 此处输出:1
  });
  // 使用命名空间-区分不同类型模块
  EventModel.create("namespace1").listen("click", function (val) {
    console.log(val); // 输出:1
  });
  EventModel.create("namespace1").trigger("click", 1);
  EventModel.create("namespace2").listen("click", function (val) {
    console.log(val); // 输出:2
  });
  EventModel.create("namespace2").trigger("click", 2);

事件代码

    var EventModel = (function () {
      var global = this,
        Event,
        _default = "default",
        Event = (function () {
          var _listen,// _下划线加英文命名私有变量
            _trigger,
            _remove,
            _slice = Array.prototype.slice,// 提取数组的slice方法
            _shift = Array.prototype.shift,// 
            _unshift = Array.prototype.unshift,
            namespaceCache = {},// 存储 命名空间
            _create,
            each = function (arr, fn) {// cache['click'],
              var ret;// 按规定方式遍历 函数数组
              for (let index = 0; index < arr.length; index++) {
                const n = arr[index];
                ret = fn.call(n, index, n);
              }
              return ret;
            };
          // 添加订阅者事件
          _listen = function (key, fn, cache) {
            if (!cache[key]) {
              cache[key] = [];
            }
            // 往存储栈中存放订阅事件
            cache[key].push(fn);
          };

          // 移除订阅者事件
          _remove = function (key, cache, fn) {
            if (cache[key]) {
              if (fn) {
                // 不传入函数则清除整个对应发布者的所有订阅者事件
                for (let index = 0; index < cache[key].length; index++) {
                  const curFn = cache[key][index];
                  if (curFn === fn) {
                    cache[key].splice(index, 1);
                    break;
                  }
                }
              } else {
                cache[key] = [];
              }
            }
          };

          _trigger = function () {
            var cache = _shift.call(arguments),
              key = _shift.call(arguments),// ['click',1].shift()=> click
              args = arguments,
              _self = this,
              ret,
              stack = cache[key];// 当前发布者 cache['click']
            if (!stack || !stack.length) {
              // 不存在发布者
              return;
            }
            // 存在则触发所有订阅者事件
            return each(stack, function () {
              //each事件--遍历执行所有的订阅者事件
              return this.apply(_self, args);
            });
          };

          _create = function (namespace) {
            var namespace = namespace || _default;
            //所有不存在命名的发布者,都存储再默认发布者中
            var cache = {},
              offlineStack = [],// 存储先触发,后订阅,这种离线状态事件
              ret = {
                listen: function (key, fn, last) {
                  _listen(key, fn, cache);// 触发订阅者事件
                  if (offlineStack === null) {
                    // 不存在离线事件
                    return;
                  }
                  if (last === "last") {
                    // 只进行最新一次的离线事件
                    offlineStack.length && offlineStack.pop()();
                  } else {
                    // 遍历所有的离线事件
                    each(offlineStack, function () {
                      this();
                    });
                  }
                  offlineStack = null;// 凡事出现订阅者之后,不在使用离线存储,说明订阅者已经上线。离线触发没有意义
                },
                one: function (key, fn, last) {
                  // 只执行这一个监听事件
                  _remove(key, cache);
                  this.listen(key, fn, last);
                },
                remove: function (key, fn) {
                  //一次监听事件
                  _remove(key, cache, fn);
                },
                trigger: function () {
                  var fn,
                    args,
                    _self = this;
                  _unshift.call(arguments, cache);
                  args = arguments;
                  fn = function () {
                    return _trigger.apply(_self, args);
                  };
                  if (offlineStack) {
                    // 存在离线存储空间。监听者未上线。事件先存在离线空间,一旦上线(产生了订阅),再执行
                    return offlineStack.push(fn);
                  }
                  return fn();
                },
              };
            return namespace
              ? namespaceCache[namespace]
                ? namespaceCache[namespace]
                : (namespaceCache[namespace] = ret)
              : ret;
          };

          return {
            create: _create,
            one: function (key, fn, last) {
              var event = this.create();
              event.one(key, fn, last);
            },
            remove: function (key, fn) {
              var event = this.create();
              event.remove(key, fn);
            },
            listen: function (key, fn, last) {
              var event = this.create();
              event.listen(key, fn, last);
            },
            trigger: function () {
              var event = this.create();
              event.trigger.apply(this, arguments);
            },
          };
        })();
      return Event;
    })();

总结

发布-订阅模式优点非常明显,一为时间上的解耦,二为对象之间的解耦。它的应用非常广泛,既可以应用再异步编程中,也可以帮助我们完成更松耦合的代码变现。无论MVC还是MVVM,都少不了发布-订阅模式的参与。

当然,发布-订阅模式也不是完全每页缺点。创建订阅者本身要消耗一定时间和内存,而且当你订阅一个消息后,也行此消息最后都未发生,但这个订阅者会始终存在于内存中。另外,发布-订阅模式虽然可以弱化对象之间的联系,但如果过度使用,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解。特别是多个发布者和订阅者嵌套到一起的时候,要跟踪一个bug不是建轻松的事情

7.命令模式

有时候需要向某些对象发送请求,但是并不知道请求的接受者是谁,也不知道被请求的操作是什么,此时希望用一种松耦合的方式来设计软件,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。

命令模式:会存在command对象和receiver对象,我们把command的动作,撤销,以及队列封装在一个commond对象中

模拟街头霸王游戏的动作回放功能

  var Ryu = {
    attack: function () {
      console.log("攻击");
    },
    defence: function () {
      console.log("防御");
    },
    jump: function () {
      console.log("跳跃");
    },
    crouch: function () {
      console.log("蹲下");
    },
  };
  //创建命令
  var makeCommand = function (receiver, state) {
    return function () {
      receiver[state]();
    };
  };
  // 键盘按钮事件
  var commands = {
    119: "jump", //W
    115: "crouch", //S
    97: "defence", //A
    100: "attack", //D
  };
  var commandStack = []; // 存储键盘按钮事件命令的队列栈
  document.onkeypress = function (ev) {
    var keyCode = ev.keyCode;
    command = makeCommand(Ryu, commands[keyCode]);
    if (command) {
      command(); // 执行命令
      commandStack.push(command); // 将执行过的任务放进队列中
    }
  };
    // <button id="replay">录像播放</button>
  document.getElementById("replay").onclick = function () {
    // 点击播放录像
    var command;
    while ((command = commandStack.shift())) {
      command();
    }
  };

8.组合模式

何时使用组合模式

  • 表示对象的部分-整体层次结构。组合模式可以方便地构造一棵树来表示对象的部分-整体结构。特别是我们在开发期间不确定这棵树到底存在多少层次的时候。在树的构造最终完成之后,只需要通过请求树的最顶层对象,便能对整棵树做统一的操作。在组合模式中增加和删除树的节点非常方便,并且符合开放-封闭原则。
  • 客户希望统一对待树中的所有对象。组合模式使客户可以忽略组合对象和叶对象的区别,客户在面对这棵树的时候,不用关心当前正在处理的对象是组合对象还是叶对象,也就不用写一堆if、else语句来分别处理它们。组合对象和叶对象会各自做自己正确的事情,这是组合模式最重要的能力。

值得注意的地方

1.组合模式不是父子关系

组合模式的树型结构容易让人误以为组合对象和叶对象是父子关系,这是不正确的。

组合模式是一种HAS-A(聚合)的关系,而不是IS-A。组合对象包含一组叶对象,但Leaf并不是Composite的子类。

组合对象把请求委托给它所包含的所有叶对象,它们能够合作的关键是拥有相同的接口。为了方便描述,本章有时候把上下级对象称为父子节点,但大家要知道,它们并非真正意义上的父子关系。

2.对叶对象操作的一致性

组合模式除了要求组合对象和叶对象拥有相同的接口之外,还有一个必要条件,就是对一组叶对象的操作必须具有一致性。

比如公司要给全体员工发放元旦的过节费1000块,这个场景可以运用组合模式,但如果公司给今天过生日的员工发送一封生日祝福的邮件,组合模式在这里就没有用武之地了,除非先把今天过生日的员工挑选出来。只有用一致的方式对待列表中的每个叶对象的时候,才适合使用组合模式。

3.双向映射关系

4.用职责链模式提高组合模式性能

9.模板方法模式

在模板方法模式中,子类实现中的相同部分被上移到父类中,而将不同的部分留待子类来实现。这也很好地体现了泛化的思想。

好莱坞原则

好莱坞无疑是演员的天堂,但好莱坞也有很多找不到工作的新人演员,许多新人演员在好莱坞把简历递给演艺公司之后就只有回家等待电话。有时候该演员等得不耐烦了,给演艺公司打电话询问情况,演艺公司往往这样回答:“不要来找我,我会给你打电话。”

在设计中,这样的规则就称为好莱坞原则。在这一原则的指导下,我们允许底层组件将自己挂钩到高层组件中,而高层组件会决定什么时候、以何种方式去使用这些底层组件,高层组件对待底层组件的方式,跟演艺公司对待新人演员一样,都是“别调用我们,我们会调用你”

理解

模板方法模式是基于继承的一种设计模式,父类封装了子类的算法框架和方法的执行顺序,子类继承父类之后,父类通知子类执行这些方法,好莱坞原则很好地诠释了这种设计技巧,即高层组件调用底层组件。

10.享元模式

享元(flyweight)模式是一种用于性能优化的模式,“fly”在这里是苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。

内部状态与外部状态

享元模式要求将对象的属性划分为内部状态与外部状态(状态在这里通常指属性)。享元模式的目标是尽量减少共享对象的数量,关于如何划分内部状态和外部状态,下面的几条经验提供了一些指引。

❏ 内部状态存储于对象内部。

❏ 内部状态可以被一些对象共享。

❏ 内部状态独立于具体的场景,通常不会改变。

❏ 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享

对象池技术

对象池的原理很好理解,比如我们组人手一本《JavaScript权威指南》,从节约的角度来讲,这并不是很划算,因为大部分时间这些书都被闲置在各自的书架上,所以我们一开始就只买一本,或者一起建立一个小型图书馆(对象池),需要看书的时候就从图书馆里借,看完了之后再把书还回图书馆。如果同时有三个人要看这本书,而现在图书馆里只有两本,那我们再马上去书店买一本放入图书馆。

对象池技术的应用非常广泛,HTTP连接池和数据库连接池都是其代表应用。在Web前端开发中,对象池使用最多的场景大概就是跟DOM有关的操作。很多空间和时间都消耗在了DOM节点上,如何避免频繁地创建和删除DOM节点就成了一个有意义的话题。

11.职责链模式

职责链模式的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

职责链模式的最大优点:请求发送者只需要知道链中的第一个节点,从而弱化了发送者和一组接收者之间的强联系。

用AOP实现职责链

function.prototype.after = function(fn){
  var self = this;
  return function(){
    var ret = self.apply(this,arguments)
    if(res === 'nextSccessor'){
      return fn.apply(this,arguments)
    }
    return ret
  }
}

12.中介者模式

迪米特法则

中介者模式是迎合迪米特法则的一种实现。迪米特法则也叫最少知识原则,是指一个对象应该尽可能少地了解另外的对象(类似不和陌生人说话)。如果对象之间的耦合性太高,一个对象发生改变之后,难免会影响到其他的对象,跟“城门失火,殃及池鱼”的道理是一样的。而在中介者模式里,对象之间几乎不知道彼此的存在,它们只能通过中介者对象来互相影响对方。

因此,中介者模式使各个对象之间得以解耦,以中介者和对象之间的一对多关系取代了对象之间的网状多对多关系。各个对象只需关注自身功能的实现,对象之间的交互关系交给了中介者对象来实现和维护。

一般来说,如果对象之间的复杂耦合确实导致调用和维护出现了困难,而且这些耦合度随项目的变化呈指数增长曲线,那我们就可以考虑用中介者模式来重构代码

13.装饰者模式

这种给对象动态地增加职责的方式称为装饰者(decorator)模式。装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法,这是一种“即用即付”的方式,比如天冷了就多穿一件外套,需要飞行时就在头上插一支竹蜻蜓,遇到一堆食尸鬼时就点开AOE(范围攻击)技能。

从功能上而言,decorator能很好地描述这个模式,但从结构上看,wrapper的说法更加贴切。装饰者模式将一个对象嵌入另一个对象之中,实际上相当于这个对象被另一个对象包装起来,形成一条包装链。请求随着这条链依次传递到所有的对象,每个对象都有处理这条请求的机会.

var before = function(fn,before){
  return function(){
    before.apply(this,arguments)
    return fn.apply(this,arguments)
  }
}

14.状态模式

状态模式是一种非同寻常的优秀模式,它也许是解决某些需求场景的最好方法。虽然状态模式并不是一种简单到一目了然的模式(它往往还会带来代码量的增加),但你一旦明白了状态模式的精髓,以后一定会感谢它带给你的无与伦比的好处。

状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。

状态模式的优缺点

状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换。

❏ 避免Context无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了Context中原本过多的条件分支。

❏ 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然。

❏ Context中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响。

状态模式和策略模式的关系

状态模式和策略模式像一对双胞胎,它们都封装了一系列的算法或者行为,它们的类图看起来几乎一模一样,但在意图上有很大不同,因此它们是两种迥然不同的模式。

策略模式和状态模式的相同点是,它们都有一个上下文、一些策略或者状态类,上下文把请求委托给这些类来执行。

它们之间的区别是策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系,所以客户必须熟知这些策略类的作用,以便客户可以随时主动切换算法;而在状态模式中,状态和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成,“改变行为”这件事情发生在状态模式内部。对客户来说,并不需要了解这些细节。这正是状态模式的作用所在。

15.适配器模式

适配器的别名是包装器(wrapper),这是一个相对简单的模式。在程序开发中有许多这样的场景:当我们试图调用模块或者对象的某个接口时,却发现这个接口的格式并不符合目前的需求。这时候有两种解决办法,第一种是修改原来的接口实现,但如果原来的模块很复杂,或者我们拿到的模块是一段别人编写的经过压缩的代码,修改原接口就显得不太现实了。第二种办法是创建一个适配器,将原接口转换为客户希望的另一个接口,客户只需要和适配器打交道。

设计原则和编程技巧

1.单一职责原则

单一职责原则(SRP)的职责被定义为“引起变化的原因”。如果我们有两个动机去改写一个方法,那么这个方法就具有两个职责。每个职责都是变化的一个轴线,如果一个方法承担了过多的职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。

SRP原则体现为:一个对象(方法)只做一件事情

SRP原则的优缺点

SRP原则的优点是降低了单个类或者对象的复杂度,按照职责把对象分解成更小的粒度,这有助于代码的复用,也有利于进行单元测试。当一个职责需要变更的时候,不会影响到其他的职责。

但SRP原则也有一些缺点,最明显的是会增加编写代码的复杂度。当我们按照职责把对象分解成更小的粒度之后,实际上也增大了这些对象之间相互联系的难度。

在设计模式中的应用

  • 单例模式
  • 装饰器模式
  • 代理模式
  • 迭代器模式

2.最少知识原则(LKP)

最少知识原则(LKP)说的是一个软件实体应当尽可能少地与其他实体发生相互作用。这里的软件实体是一个广义的概念,不仅包括对象,还包括系统、类、模块、函数、变量等。

减少对象之间的联系

最少知识原则要求我们在设计程序时,应当尽量减少对象之间的交互。如果两个对象之间不必彼此直接通信,那么这两个对象就不要发生直接的相互联系。常见的做法是引入一个第三者对象,来承担这些对象之间的通信作用。如果一些对象需要向另一些对象发起请求,可以通过第三者对象来转发这些请求。

外观模式

外观模式的作用是对客户屏蔽一组子系统的复杂性。外观模式对客户提供一个简单易用的高层接口,高层接口会把客户的请求转发给子系统来完成具体的功能实现。大多数客户都可以通过请求外观接口来达到访问子系统的目的。但在一段使用了外观模式的程序中,请求外观并不是强制的。如果外观不能满足客户的个性化需求,那么客户也可以选择越过外观来直接访问子系统。

在设计模式中的应用

  • 中介者模式
  • 外观模式

封装在最少知识原则中的体现

封装在很大程度上表达的是数据的隐藏。一个模块或者对象可以将内部的数据或者实现细节隐藏起来,只暴露必要的接口API供外界访问。对象之间难免产生联系,当一个对象必须引用另外一个对象的时候,我们可以让对象只暴露必要的接口,让对象之间的联系限制在最小的范围之内。

3.开放-封闭原则(OCP)

现在可以引出开放-封闭原则的思想:当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,但是不允许改动程序的源代码

用对象的多态性消除条件分支

其他方式可以帮助我们编写遵守开放-封闭原则

1.放置挂钩

放置挂钩(hook)也是分离变化的一种方式。我们在程序有可能发生变化的地方放置一个挂钩,挂钩的返回结果决定了程序的下一步走向。这样一来,原本的代码执行路径上就出现了一个分叉路口,程序未来的执行方向被预埋下多种可能性。

2.使用回调函数

在JavaScript中,函数可以作为参数传递给另外一个函数,这是高阶函数的意义之一。在这种情况下,我们通常会把这个函数称为回调函数。在JavaScript版本的设计模式中,策略模式和命令模式等都可以用回调函数轻松实现。

回调函数是一种特殊的挂钩。我们可以把一部分易于变化的逻辑封装在回调函数里,然后把回调函数当作参数传入一个稳定和封闭的函数中。当回调函数被执行的时候,程序就可以因为回调函数的内部逻辑不同,而产生不同的结果。

在设计模式中的应用

  • 发布-订阅模式
  • 模板方法模式
  • 策略模式
  • 代理模式
  • 职责链模式

接口和面向接口编程

代码重构

1.提炼函数

❏ 避免出现超大函数。

❏ 独立出来的函数有助于代码复用。

❏ 独立出来的函数更容易被覆写。

❏ 独立出来的函数如果拥有一个良好的命名,它本身就起到了注释的作用。

2.合并重复的条件片段

如果一个函数体内有一些条件分支语句,而这些条件分支语句内部散布了一些重复的代码,那么就有必要进行合并去重工作。

3.把条件分支语句提炼成函数

这句代码要表达的意思很简单,就是判断当前是否正处于夏天(7~10月)。尽管这句代码很短小,但代码表达的意图和代码自身还存在一些距离,阅读代码的人必须要多花一些精力才能明白它传达的意图。

4.合理使用循环

在函数体内,如果有些代码实际上负责的是一些重复性的工作,那么合理利用循环不仅可以完成同样的功能,还可以使代码量更少。

5.提前让函数退出代替嵌套条件分支

使得if()提前return,避免过多的嵌套

6.传递对象参数代替过长的参数列表

有时候一个函数有可能接收多个参数,而参数的数量越多,函数就越难理解和使用。使用该函数的人首先得搞明白全部参数的含义,在使用的时候,还要小心翼翼,以免少传了某个参数或者把两个参数搞反了位置。

这时我们可以把参数都放入一个对象内,然后把该对象传入setUserInfo函数,setUserInfo函数需要的数据可以自行从该对象里获取。

7.尽量减少参数数量

如果调用一个函数时需要传入多个参数,那这个函数是让人望而生畏的,我们必须搞清楚这些参数代表的含义,必须小心翼翼地把它们按照顺序传入该函数。而如果一个函数不需要传入任何参数就可以使用,这种函数是深受人们喜爱的。在实际开发中,向函数传递参数不可避免,但我们应该尽量减少函数接收的参数数量。

8.少用三目运算符

即使我们假设三目运算符的效率真的比if、else高,这点差距也是完全可以忽略不计的。在实际的开发中,即使把一段代码循环一百万次,使用三目运算符和使用if、else的时间开销处在同一个级别里。

同样,相比损失的代码可读性和可维护性,三目运算符节省的代码量也可以忽略不计。让JS文件加载更快的办法有很多种,如压缩、缓存、使用CDN和分域名等。把注意力只放在使用三目运算符节省的字符数量上,无异于一个300斤重的人把超重的原因归罪于头皮屑。

9.合理使用链式调用

使用链式调用的方式并不会造成太多阅读上的困难,也确实能省下一些字符和中间变量,但节省下来的字符数量同样是微不足道的。链式调用带来的坏处就是在调试的时候非常不方便,如果我们知道一条链中有错误出现,必须得先把这条链拆开才能加上一些调试log或者增加断点,这样才能定位错误出现的地方。

如果该链条的结构相对稳定,后期不易发生修改,那么使用链式调用无可厚非。但如果该链条很容易发生变化,导致调试和维护困难,那么还是建议使用普通调用的形式.

10.分解大型类

11.用return退出多重循环

假设在函数体内有一个两重循环语句,我们需要在内层循环中判断,当达到某个临界条件时退出外层的循环。我们大多数时候会引入一个控制标记变量:符合

可以把循环后面的代码放到return后面,如果代码比较多,就应该把它们提炼成一个单独的函数