子慕谈设计模式系列(二)——设计模式六大原则

六大原则

  • 单一职责原则
  • 里氏替换原则
  • 依赖倒置原则
  • 接口隔离原则
  • 迪米特法则
  • 开闭原则

 

前言

设计模式不容易用文字描述清楚,而过多的代码,看起来也让人摸不到头脑,加上词语或者文字描述的抽象感,很容易让人看了无数设计模式的文章,也仍然理解不了。  所以我一直打算写此系列博客,首先我会从大量文章里去理解这些设计模式,最后我用自己的语言组织转化为博客,希望用更少的代码,更容易理解的文字,来聊一聊这些设计模式。  我所理解、所描述的每一个设计模式也可能有些是错误的,甚至也不一定有非常深刻的理解,所以希望有人指出,我可以更改博客内容。  因为我是前端,所以设计模式的代码以前端代码和视角为主。  此博客内容对每一种模式并不会写得非常深入,也许能为读者打通一些认知,如果看了此系列博客,再去看其他更深入的博客,可能是一种比较好的方式。

 

单一职责原则

单一职责原则很简单,一个方法 一个类只负责一个职责,各个职责的程序改动,不影响其它程序。  这是常识,几乎所有程序员都会遵循这个原则。

 

里氏替换原则

一位姓里的女士提出来的,所以叫里氏替换原则。  通俗解释此原则: 子类可以扩展父类的功能,但不能改变父类原有的功能。  它包含以下4层含义:

  • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
  • 子类中可以增加自己特有的方法。
  • 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

① 子类可以实现父类的抽象方法,但不能覆盖(重写)父类的非抽象方法

方法覆盖又称方法重写,所以我理解这里的覆盖就是重写吧。  当子类继承了父类,有些情况下可能还是需要重写继承的方法。  但是重写确实会给系统造成一些麻烦,特别是重写的次数变多了之后,后期维护或者迭代的过程中容易概念混淆,逻辑混淆,加大犯错的风险。父类的方法应该尽量稳定。

 

② 子类中可以增加自己特有的方法。

子类继承父类,肯定是需要子类有自己特有方法的,否则就没必要继承了。

 

③ 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。

输入的参数更宽松可以理解为“更大的范围或者定义”, 父类定义的入参要足够宽泛,覆盖子类需求,子类的参数应该在父类定义的范围内,但是当父类方法不能满足子类的情况下,出现重载或者重写,这时候输入的参数不放大的话,是没法满足业务需求的 。 

有一个例子:鸵鸟不是鸟。  按照鸟的定义鸵鸟确实是鸟(恒温动物,卵生,全身披有羽毛,身体呈流线形,有角质的喙,眼在头的两侧。前肢退化成翼,后肢有鳞状外皮,有四趾),那么鸵鸟继承于鸟这个基类,印象中鸟都是能飞的,所以鸟类定义一个飞行速度参数。  因为鸵鸟不能飞,就只能把速度定义为0。  现在出现一个业务,需要计算每个鸟类飞过黄河的时间,可是鸵鸟速度为0,时间永远无法得出,这个就造成业务无法顺利进行了,鸵鸟不能完全替代鸟,这也就违背了里氏替换原则。  所以这里不能直接用鸟类来计算飞行时间,而是应该删除鸟类的飞行速度参数,生成一个子类:飞鸟类,给飞鸟类定义飞行速度,去计算所有飞鸟类的飞行时间,这样才能满足需求。 

需要注意两点,1对类的继承关系的定义要搞清楚,2设计要依赖于具体行为和环境。总之子类可以随便扩展,但是别改父类。

 

④ 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

JS不需要定义抽象方法,返回值也是不用申明的。  对于java来说,抽象方法只需要定义,方法体为空,当然也没必要申明返回,这个规则对java来说自然就已经遵循了。

 

依赖倒置原则

  • 高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。
  • 抽象不应该依赖于具体实现,具体实现应该依赖于抽象。
① 高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。

先说说什么是高层模块什么是底层模块。  高层模块调用底层模块,被另一个模块调用的模块就叫底层模块。在传统的应用架构中,低层次的组件设计用于被高层次的组件使用,这一点提供了逐步的构建一个复杂系统的可能。  在这种结构下,高层次的组件直接依赖于低层次的组件去实现一些任务,这种对于低层次组件的依赖限制了高层次组件被重用的可行性。  而依赖倒置原则使得高层次的模块不依赖于低层次的模块的实现细节,把高层次模块从对低层次模块的依赖中解耦出来,从而使得低层次模块依赖于高层次模块的需求抽象,当高层模块需要使用底层模块,便引用此底层模块。  回想一下angular的依赖注入,就是这种。  下面第二条的例子也可以帮助更好的理解。

 

② 抽象不应该依赖于具体实现,具体实现应该依赖于抽象。
具体实现依赖于抽象,举个例子,造一辆车,需要车架,轮子,沙发等。先搭好架子,定义一个具体实现类,安装车架,放置沙发,安装轮胎。具体实现流程已经具备,并且也定义好了抽象方法。
Car(){
    var body = new Body();
    var tyre = new Tyre();
    var sofa = new Sofa();
    this.setBody(body);
    this.setTyre(tyre);
    this.setSofa(sofa);
    return this;
}
那么接下来就是造车架、造轮子、造沙发。
Body(){
  //to do something
  return this;
}
Tyre(){
  //to do something
  return this;
}
Sofa(){
  //to do something
  return this;
}

反过来,抽象依赖于具体实现,举个例子(把上面的改造一下):造一辆车,先造个轮子,根据轮子再去做个车架,根据车架再去完成沙发。

Car(){
    var tyre = new Tyre();
    this.setTyre(tyre);

    var body = new Body(tyre);
    this.setBody(body);

    var sofa = new Sofa(body);
    this.setSofa(sofa);
    return this;
}
Body(tyre){
  //to do something
  return this;
}
Tyre(){
  //to do something
  return this;
}
Sofa(body){
  //to do something
  return this;
}

这样的话,如果轮胎变化了,可能就会影响整个后面流程。  总结一下:具体实现依赖抽象,就是提前定义和约定好每个抽象,然后分别去实现抽象,再去具体实现,做到心中有数,各个击破。  而抽象依赖具体实现,就是先做一个事情,再考虑下一个事情,没有提前规划完善,过程中就容易出问题。

 

接口隔离原则

客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。  这个原则很简单,盗用两个图如下,对于java等语言来说,B和D类依赖“接口I“(图1),但是B并没有使用方法4、5,D没有使用方法2、3,但是因为依赖接口I,所以也需要定义并不使用的方法。  从前端角度来说,一个公共类它可能被不同的其它类引用,但是每个类只需要用到公共类的其中一个方法,但是却需要把公共类全部引入,这样就显得太臃肿。  所以通过把一个大接口拆分成几个小接口,可以使代码更精准,引用更灵活。  接口可以尽量小,但是要有限度。  对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化,所以一定要适度。

 

 

 

迪米特法则

 迪米特法则又叫作最少知道原则,就是说一个对象应当对其他对象有尽可能少的了解。  举个例子:我们定义一个类,定义一个变量,会使用get,set方法来控制这个变量读写代码如下:

var obj = {
  val:0,
  setVal:function(val){
      this.val = val;
      alert(val);
  },
  getVal:function(val){
      return this.val;
  }
}

function xxx(){
    obj.setVal(666);
}

 

如果我们需要在修改val值的时候,弹出一个提示框告诉我们最新的值,那么我们可以把alert写在setVal方法内,在其他对象中使用obj对象的时候,只需要使用setVal方法,obj内部发生了什么当前类并不知道,当前类只能用暴露出来的set方法,而不需要知道set方法做了什么事情。  反过来,我们抛弃这个原则写一个代码例子:

var obj = {
  val:0
}

function xxx(){
    obj.val = 666;
    alert(obj.val);
}

这样xxx方法就非常了解obj这个对象了,因为在它内部直接操作了obj对象。真的如此就一点封装都没有了,在其他地方也会出现非常多的重复代码。  通俗的来讲,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供公共方法,不对外泄漏任何信息。

 

开闭原则

软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的。  在软件开发和迭代过程中,常常可能需要修改逻辑,比如一个公用的方法Func在不同地方被使用,在某一个方法体中需要修改这个公用方法的逻辑来达到当前的需求,但是修改此公用方法Func必然会影响到其他地方,然而其他地方需要保持原方法的逻辑,所以这里修改是被禁止的。  那么可以新增一个方法Func2,替换当前方法的引用,如此就是所谓的扩展,扩展是开放的。

再举一个常见的例子,对于订单数据,最开始我们定义了订单的状态status: 1-2分别代表 未完成和已完成。后来订单开始付费了,我们需要更多的状态,已支付和未支付。  那么支付状态和订单完成状态并不完全独立,要能同时表示用一个字段表示这两种状态,需要改变status的定义,比如1代表未完成并且未支付,2代表未完成并且已支付,如此修改之后,以前的所有判断和逻辑会发生变化,旧的数据也无法兼容,系统几乎很难迭代下去,代价也很大。  那么如果扩展一个字段payStatus,就不需要修改数据库定义,前端也只需要在以前的逻辑上多一个payStatus状态的判断。  开闭原则的扩展开放,修改封闭,是很有必要的。

 

此系列博客目录:

 

子慕谈设计模式系列(一)

子慕谈设计模式系列(二)——设计模式六大原则

子慕谈设计模式系列(三)

 

posted @ 2017-10-11 09:18  子慕大诗人  阅读(965)  评论(0编辑  收藏  举报