编码的设计原则--为啥使用设计模式

单一原则(SRP

对于一个类而言,应该只有一个可以引起它变化的原因,在JavaScript中,需要用到类的场景并不是很多,所以单一职责原则更多的运用到对象或是方法级别上面,因此对于前端开发的我们来说,我们主要讨论的大多是对象和方法。
单一职责原则(SRP)的职责被定义为:“引起变化的原因”,如果我们有2个动机去修改一个方法,那么这个方法就有2个职责、每一个职责都有一个轴线,如果一个方法承担过多的职责,那么在需求变迁过程中,这个方法被修改的概率就越大。我们也知道,当一个方法通常为一个不稳定的方法,那么修改代码通常是一件很危险的事情,特别是当两个职责耦合在一起的时候,一个职责发生变化的时候,很容易会影响其他的职责,造成意想不到破坏,这种耦合性的带的是低内聚和脆弱的设计
SRP原则体现为:一个对象(方法)只有一个事情。

设计模式与SRP原则

单例模式

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

这个是我们最开始编写的代码,现在我们把管理单例的职责和创建浮窗的职责分别封装到两个方法中。这两个方法可以独立便捷化而不相互的影响,当我们链接在一起的时候,就完成可创建唯一浮窗的功能,实现了职责的分离,优化了代码,下面的代码显然是更好的做法:

let getSingle = function (fn){
  let result;
  return function(){
    return resukt || (result = fn.apply(this,arguments));
  }
};
let  createLoginLayer = function(){
  let div = document.createElement('div');
   div.innerHTML = '我是登录浮窗';
   document.body.appendChild(div);
    return div;
};
let createLoginLayer = getSingle (createLoginLayer);
let loginLayer1 = createLoginLayer ();
let loginLayer2 = createLoginLayer ();

console.log(loginLayer1 === loginLayer2); //true

代理模式
迭代器模式
装饰者模式

什么时候应该进行职责的分离:

SRP原则是最简单也是最难正确运用原理,因为在我们的项目中并不是所有的职责都应该一一分离:
一方面:如果随着需求的变化,有两个职责总是一起变化的,那么就没有不要分离他们。比如在ajax请求的时候,创建Xhr对象和发送xhr请求几乎总是在一起的,那么创建xhr对象的职责和发送xhr请求的职责就没有必要分离开来。
另一方面:职责的变化轴线仅当他们确定会发生变化的时候才有意义,即使两个职责已经被耦合到一起,但他们还没有发生改变的征兆,那么也许没有必要分离它们,在代码需要重构的时候在进行分离也不迟。

SRP原则的优缺点

SPR的优点:降低了我单个类或是对象的复杂度,按照职责把对象划分为更小的粒度,有利于代码的复用,有利于代码的复用也有利于进行单元测试,当一个职责需要变更的时候,并不会影响其他。
SPR的缺点:明显的增加代码的复杂度,当我们按照职责把对象分解为更小的粒度之后,实际上也增大了这些对象相互联系的难度。

最少知识原则(LKP)迪米特法则

最少知识原则说的是一个软件实体应当尽可能少的与其他实体发生相互作用。这里的软件是一个广义的概念:包括对象、系统、类、模块、函数、变量等。由于我们是进行前端开发,所以我们主要针对对象来阐述这个问题,我们具体举一个例子来进行说明:

某个军队的将军需要挖掘一些散兵坑,下面是完成这个任务的方式:将军通知上校让他叫来少校,然后让少校找到中尉,并让中尉通知一个军士,最后军士叫来了一个士兵,然后命令这个士兵挖一些散兵坑。

这样的方式我们听起来感觉很荒谬,我们现在模仿这个实现方式来编写一段代码:

gerneral.getColonel(c).getMajor(m).getCaotain(c).getSergeant(s).getPrivate(p).digFoxhole();

代码通过很长的消息链才完成了一个任务,这就像让将军通过那么多反思的步骤才可以命令别人挖掘散兵坑一样荒谬,而且,这一条链上任何一个对象发生改变都会影响整条链的结果。
其实最可能的结果是:将军自己根本不会考虑挖散兵坑这样的细节信息,即使考虑也是这样操作:“我不关心这个工作如何完成,但是你得命人去挖散兵坑”。
LKP原则要求我们在设计程序时,应当尽量减少对象之间的交互,如果两个对象不必直接通信,那么对象之间就不要发生直接的相互联系,常见的做法是引入一个第三者对象,来承担对象与对象之间的通信。如果一些对象需要向另一些对象发起请求,可以通过第三者对象来转发这些请求。

设计模式中的最少知识原则

中介者模式
外观模式

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

封装在很大的程度上表达的是数据的隐蔽,一个模块或是对象可以将内部的数据或是实现细节隐藏起来,只要暴露必要的接口API供外界,对象之间难免会产生一些联系,当一个对象必须引用另外一个对象的时候,我们可以让对象只暴露必要的借楼,让对象之间的联系限制在最小的范围内。
同时,封装也用来限制变量的作用域,在JavaScript中对作用域的规定是:

  • 变量在全局声明,或者只代码的任何位置隐式申明,则变量在全局可见
  • 变量在函数内显示声明,在函数内可见

把变量法人可见性限制在一个尽可能小的范围内,这个变量对其他不相关模块的影响越小,变量被改写和冲突的概率就越小,这也是广义的最少知识原则的一种体现。
假如我们要编写一个具有缓存效果的计算乘积的函数function mult(){},我们需要一个对象let cache ={},来保存计算过的结果,cache对象夏然只对mult有用,把cache对象放在mult形成的闭包中,显然比把它放在全局作用域中更加合适。

let mult = (function(){
  let cache = {};
  return function(){
    let args = Array.prototype.join.call(arguments, ',');
    if(cache[args]){
      return cache[args];
    }
    let a = 1;
    for(let i = 0. l  = arguments.length; i< l; i++){
      a = a * arguments[i];
    }
    return cache [args] = a;
  }
})();
mult (1,2,3) //输出为6

开放- 封闭原则

在面向对象的程序设计中,开放-封闭原则(OCP)是最重要的一条原则,很多的时候,一个程序具有良好的设计,往往说明它是符合开放-封闭原则的。
开放- 封闭原则定义:软件实体(类、模块、函数)等应该是可以扩展的,但是不可以修改。
这个概念的提出是源于我们在接到一个新的需求,这个需求是要在一个功能原有的基础上添加新的功能,我们是知道的,我们一般最常见的方法就是,深入到源代码,然后在源代码的基础上进行修改,但是,这样的行为对于程序来说是一种危险的行为,也许我们会遇到bug越改越多的情况。在这样的场景上,我们提出了开放- 封闭原则。

开放与封闭

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

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

过多的条件分支是造成程序违反开放-封闭原则的一个常见原因,每当需要增加一个新的if语句时,就要被迫改变原函数。把if换成switch-case是没有用的,这是一种换汤不换药的做法,实际上,每当我们看到一大片的if或是switch-case语句时,第一时间就应该考虑,能否利用对象的多态来重构它们。
我们通过下面一段代码的例子来进行具体的阐述:

let makeSound = function(animal){
  if(animal instanceof Duck){
    console.log("嘎嘎嘎");
  }else if(animal instanceof Chicken){
    console.log("咯咯咯");
  }
};
let Duck = function(){};
let Chicken = function(){};

makeSound(new Duck());
makeSound(new Chicken());

但是如果我们想要添加一种动物的话,按照现在的趋势,我们一定要修改源代码,或是在源代码中添加新的东西。那么当我们添加一只狗,那么我们势必要修改makeSound的代码:

let makeSound = function(animal){
  if(animal instanceof Duck){
    console.log("嘎嘎嘎");
  }else if(animal instanceof Chicken){
    console.log("咯咯咯");
  }else if(animal instanceof Dog){
    console.log("汪汪汪");
  }
};
let Dog= function(){};
makeSound(new Dog());

利用多态的思想,我们把程序中不变的部分提取出来(动物都会叫),然后把可变的部分封装起来,这样一来程序就有可扩展性,现在看一下我们具体是怎么实现的:

let makeSound = function(animal){
  animal.sound();
};
let Duck = function(){};

Duck.prototype.sound = function(){
  console.log("嘎嘎嘎");
}

let Chicken= function(){};

Chicken.prototype.sound = function(){
  console.log("咯咯咯");
}

makeSound(new Duck());
makeSound(new Chicken());

//增加动物狗,而不修改动物原来的makesound函数
let Dog = function(){};
Dog.prototype.sound = function(){
  console.log("汪汪汪");
}
makeSound(new Dog());
找出变化的地方

我们通过封装变化的凡是,可以将系统中稳定不变的东西和容易发生改变的额东西,分割开来,在系统演变的过程中,我们只需要替换哪些容易发生变化的部分,稳定的部分不需要改变。
在上面的例子中,由于每种动物的叫声是不同的,所以动物具体怎么叫是可变的,于是我们把动物具体怎么叫的逻辑从makeSound中提炼了出来,而动物都会叫这一点是不变的,makeSound函数里实现的逻辑只和动物都会叫有关,这样一来,makeSound就成了一个简单的封闭的函数。现在我们看一下我们可以通过什么方式来帮助我们编写遵循开闭原则的代码呢?
放置倒钩
我们在程序有可能发生变化的地方,放置一个挂钩,挂钩返回的结果,决定程序的下一步应该怎么走,这样一来,元原本代码路径上就会出现一个岔路,程序未来执行的方向也会埋下很多的可能。在模板方法的模式中我们就用到了挂钩。
使用回调函数
JavaScript中,韩式可以作为一个参数传递给另外的一个函数,这也是高阶函数的意义之一。在这种情况下,我们通常会
把这个函数叫做回调函数。在JavaScript的实际模式中,策略模式和命令模式都可以使用回调函数进行轻松的实现。

设计模式中的开放-封闭原则

订阅-发布模式
发布-订阅模式用来降低多个对象之间的依赖关系,它可以完全取代对象之间的硬编码的通信机制,一个对象不用再显示的调用另外一个对象的某个接口。当新的订阅者出现的时候,发布者的代码不需要进行任何的修改;同样当发布者需要改变的时候,也不会影响之前的订阅者。
策略模式
策略模式将各种算法都封装成单独的策略类,这些策略类可以交换使用,策略和使用策略的客户代码可以分别单独进行丢该而不相互影响。我们新增一个策略类也是非常的方便,完全不用修改之前的代码。
代理模式
职责链模式
模板方法模式

开放-封闭原则的相对性

开发也不能做到完全的开放,而且做到完全的封闭,也是不容易做到的,就算在技术上可以做到,也要耗费太多的时间和精力,所以我们只需要做到下面这两点即可:

  • 挑出最容易发生变化的地方,然后构造抽象来封闭这些变化
  • 在不可避免发生修改的时候,尽量修改那些 相对容易修改的地方。对于一个开源库来说,修改它提供的配置文件,要比修改它的源代码简单的多。

ps:这一篇文章,因为没有将设计模式融汇贯通,所以在阐述一些例子的时候,没有进行展开叙述,后期会对改文章进行持续的更新。

你可能感兴趣的:(编码的设计原则--为啥使用设计模式)