六大设计原则(设计模式之禅读书笔记)

[TOC]

单一设计原则---(专人专事)

单一职责原则的定义是:应该有且仅有一个原因引起类的变更。

image.png

优化:
重新拆封成两个接口,IUserBO负责用户的属性,简单地说,IUserBO的职责就是收集和 反馈用户的属性信息;IUserBiz负责用户的行为,完成用户信息的维护和变更。

image.png
image.png

简单理解就是 get set 一起,其他操作封装成biz(biz是Business的缩写,实际上就是控制层(业务逻辑层)),当然不局限于这种类型的对象。

注意 单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或 类设计得是否优良,但是“职责”和“变化原因”都是不可度量的,因项目而异,因环境而异

单一职责适用于接口、类,同时也适用于方法,什么意思呢?一个方法尽可能做一件事 情,比如一个方法修改用户密码,不要把这个方法放到“修改用户信息”方法中,这个方法的 颗粒度很粗。
如果要修改用户名称,就调用changeUserName方法;要修改家庭地址, 就调用changeHomeAddress方法;要修改单位电话,就调用changeOfficeTel方法。每个方法的 职责非常清晰明确,不仅开发简单,而且日后的维护也非常容易,大家可以逐渐养成这样的 习惯。

这个单一原则重在理解,不能认死理,拆分太严重也会导致类或者方法数过多,具体情况具体分析吧。

里氏替换原则---(继承规范)

主要是为良好的继承定义了一个规范。

这个规范就是:所有引用基类的地方必须能透明地使用其子类的对象(不会改变任何逻辑)
通俗点讲,只要父类能出现的地方子类就可以出现,而且 替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但 是,反过来就不行了,有子类出现的地方,父类未必就能适应。
更正宗的定义:如果对每一个类型为S的对象o1,都有类型为T的对 象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变 化,那么类型S是类型T的子类型。

说明:这是给继承定义的一种良好的规范,现实中可能会出现不符合这种原则的代码,所以这是规范,并不是所有都是这样的。

细分里氏替换原则四种含义:

  • 子类必须完全实现父类的方法(这里的实现应该是要保证都有方法体的意思)
    比如说父类是个枪,定义了一个方法“射击”,那么任意子类(玩具枪、狙击枪、步枪)都应该能调用射击这个方法。
  • 子类可以有自己的个性
    当然子类可以增加一些方法或者复写一些方法
  • 覆盖或实现父类的方法时输入参数可以被放大
    范例:
public class Father {
 public Collection doSomething(HashMap map){
   System.out.println("父类被执行...");
   return map.values();
 }
}
public class Son extends Father {
  //放大输入参数类型
  public Collection doSomething(Map map){
    System.out.println("子类被执行...");
    return map.values();
  }
}

请注意粗体部分,与父类的方法名相同,但又不是覆写(Override)父类的方法。你加 个@Override试试看,会报错的,为什么呢?方法名虽然相同,但方法的输入参数不同,就 不是覆写,那这是什么呢?是重载(Overload)!不用大惊小怪的,不在一个类就不能是重 载了?继承是什么意思,子类拥有父类的所有属性和方法,方法名相同,输入参数类型又不 相同,当然是重载了。

父类使用场景:

public class Client {
   public static void invoker(){
      //父类存在的地方,子类就应该能够存在
      Father f = new Father();
      HashMap map = new HashMap();
      f.doSomething(map);
   }
   public static void main(String[] args) {
      invoker();
   }
}

运行结果:父类被执行...
根据里氏替换原则在这里使用子类替换:

public class Client {
   public static void invoker(){
      //父类存在的地方,子类就应该能够存在
      Son f =new Son();
      HashMap map = new HashMap();
      f.doSomething(map);
   }
   public static void main(String[] args) {
      invoker();
   }
}

运行结果:父类被执行...
运行结果还是一样,看明白是怎么回事了吗?父类方法的输入参数是HashMap类型,子 类的输入参数是Map类型,也就是说子类的输入参数类型的范围扩大了,子类代替父类传递 到调用者中,子类的方法永远都不会被执行。这是正确的,如果你想让子类的方法运行,就 必须覆写父类的方法。

对调参数:

public class Father {
 public Collection doSomething(Map map){
   System.out.println("父类被执行...");
   return map.values();
 }
}
public class Son extends Father {
  //放大输入参数类型
  public Collection doSomething(HashMap map){
    System.out.println("子类被执行...");
    return map.values();
  }
}

如果依然使用上面的使用场景运行结果就会变成这样:
运行结果:父类被执行...
更换子类
运行结果:子类被执行...

这就不正常了,子类在没有覆写父类的方法的前提下,子类方法被执行了,这会引起业务 逻辑混乱 ,“歪曲”了父类的意图,引起一堆意想不到的业务逻辑混乱,所以子类中方法的前置条 件必须与超类中被覆写的方法的前置条件相同或者更宽松。

  • 覆写或实现父类的方法时输出结果可以被缩小
    这是什么意思呢,父类的一个方法的返回值是一个类型F,子类的相同方法(重载或覆 写)的返回值为S,那么里氏替换原则就要求S必须小于等于F,也就是说,要么S和F是同一 个类型,要么S是F的子类,为什么呢?分两种情况,如果是覆写,父类和子类的同名方法的 输入参数是相同的,两个方法的范围值S小于等于F,这是覆写的要求,这才是重中之重,子 类覆写父类的方法,天经地义。如果是重载,则要求方法的输入参数类型或数量不相同,在 里氏替换原则要求下,就是子类的输入参数宽于或等于父类的输入参数,也就是说你写的这 个方法是不会被调用的,参考上面讲的前置条件。

综合3、4条可以总结一句话:子类入参类型可以放大范围(可以是父入参的父类),输出结果要缩小范围(可是父出参的子类)。
理解起来比较困难。

依赖倒置原则---(面向接口编程)

  • 高层模块不应该依赖低层模块,两者都应该依赖其抽象;
  • 抽象不应该依赖细节;
  • 细节应该依赖抽象。
    高层模块和低层模块容易理解,每一个逻辑的实现都是由原子逻辑组成的,不可分割的 原子逻辑就是低层模块,原子逻辑的再组装就是高层模块。那什么是抽象?什么又是细节 呢?在Java语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的;细节就是实 现类,实现接口或继承抽象类而产生的类就是细节,其特点就是可以直接被实例化,也就是 可以加上一个关键字new产生一个对象。
    依赖倒置原则在Java语言中的表现就是:
  • 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过 接口或抽象类产生的;
  • 接口或抽象类不依赖于实现类;
  • 实现类依赖接口或抽象类。
    更加精简的定义就是“面向接口编程”——OOD(Object-Oriented Design,面向对象设 计)的精髓之一。

举个例子:
司机开动奔驰车:

image.png

这样设计的话突然来个宝马,司机没有对应开宝马的方法,就不能执行了。
司机类和奔驰车类之间是紧耦合的关系,其导致的结果就是系统的可维护性大大降低。

对上面的例子进行优化,引入依赖倒置 原则后的类图如图3-2所示

image.png

建立两个接口:IDriver和ICar,分别定义了司机和汽车的各个职能,司机就是驾驶汽 车,必须实现drive()方法

在业务场景中,我们贯彻“抽象不应该依赖细节”,也就是我们认为抽象(ICar接口)不 依赖BMW和Benz两个实现类(细节),因此在高层次的模块中应用都是抽象
代码如下:

    public interface ICar {
        //是汽车就应该能跑
        public void run();
    }
    public class Benz implements ICar{
        //汽车肯定会跑
        public void run(){
            System.out.println("奔驰汽车开始运行...");
        }
    }
    public class BMW implements ICar{
        //宝马车当然也可以开动了
        public void run(){
            System.out.println("宝马汽车开始运行...");
        }
    }
  public class Driver implements IDriver{
        //司机的主要职责就是驾驶汽车
        public void drive(ICar car){
            car.run();
        }
    }
  • 接口和抽象类都是属于抽象的,有了抽象才可能依赖倒置。
  • 变量的表面类型尽量是接口或者是抽象类

讲了这么多,估计大家对“倒置”这个词还是有点不理解,那到底什么是“倒置”呢?我们 先说“正置”是什么意思,依赖正置就是类间的依赖是实实在在的实现类间的依赖,也就是面 向实现编程,这也是正常人的思维方式,我要开奔驰车就依赖奔驰车,我要使用笔记本电脑 就直接依赖笔记本电脑,而编写程序需要的是对现实世界的事物进行抽象,抽象的结果就是 有了抽象类和接口,然后我们根据系统设计的需要产生了抽象间的依赖,代替了人们传统思 维中的事物间的依赖,“倒置”就是从这里产生的。

接口隔离原则---(定义接口规范)

  1. 客户端不应该依 赖它不需要的接口

依赖它需要的接口,客 户端需要什么接口就提供什么接口,把不需要的接口剔除掉,那就需要对接口进行细化,保 证其纯洁性

  1. 类间的依赖关系应该建立在最小的接口上

它要求是最小 的接口,也是要求接口细化,接口纯洁,与第一个定义如出一辙,只是一个事物的两种不同 描述。

总结上面两句话:

  • 建立单一接口,不要建立臃肿庞大的接口。再通 俗一点讲:接口尽量细化,同时接口中的方法尽量少。
    ** 看到这里大家有可能要疑惑了,这与 单一职责原则不是相同的吗?错,接口隔离原则与单一职责的审视角度是不相同的,单一职 责要求的是类和接口职责单一,注重的是职责,这是业务逻辑上的划分,而接口隔离原则要 求接口的方法尽量少。例如一个接口的职责可能包含10个方法,这10个方法都放在一个接口 中,并且提供给多个模块访问,各个模块按照规定的权限来访问,在系统外通过文档约 束“不使用的方法不要访问”,按照单一职责原则是允许的,按照接口隔离原则是不允许的, 因为它要求“尽量使用多个专门的接口”。专门的接口指什么?就是指提供给每个模块的都应 该是单一接口,提供给几个模块就应该有几个接口,而不是建立一个庞大的臃肿的接口,容 纳所有的客户端访问。**

实际应用上面就是一个接口里面不要写太多方法,如果确实需要很多方法的话,应该尽量根据实际需求进行拆分,拆分成多个接口,按需实现。

例如:对美女的定义 面貌、身材和气质 定义了如下接口

image.png

然而每个人的审美观不同,并不是所有的人都认为美女都是这三种条件的
比如唐朝 身材就认为胖点的好
所以接口要进行拆分,按需进行实现

image.png

接口是我们设计时对外 提供的契约,通过分散定义多个接口,可以预防未来变更的扩散,提高系统的灵活性和可维 护性。

根据接口隔离原则拆分接口时,首先必须满足单一职责原则。

迪米特法则---(低耦合)

对类的低耦合提出了明确的要求

  1. 只和朋友交流
    老师想让体育委员确认一下全班女生来齐没有,就对他 说:“你去把全班女生清一下。
image.png
image.png

场景类:

image.png

首先确定Teacher类有几个朋友类,它仅有一个朋友类—— GroupLeader。为什么Girl不是朋友类呢?Teacher也对它产生了依赖关系呀!朋友类的定义是 这样的:出现在成员变量、方法的输入输出参数中的类称为成员朋友类,而出现在方法体内 部的类不属于朋友类,而Girl这个类就是出现在commond方法体内,因此不属于Teacher类的 朋友类。迪米特法则告诉我们一个类只和朋友类交流,但是我们刚刚定义的commond方法却 与Girl类有了交流,声明了一个List动态数组,也就是与一个陌生的类Girl有了交流, 这样就破坏了Teacher的健壮性。方法是类的一个行为,类竟然不知道自己的行为与其他类 产生依赖关系,这是不允许的,严重违反了迪米特法则。

所以应该修改调整一下:

image.png
image.png

场景类:

image.png

对程序进行了简单的修改,把Teacher中对List的初始化移动到了场景类中,同时 在GroupLeader中增加了对Girl的注入,避开了Teacher类对陌生类Girl的访问,降低了系统间 的耦合,提高了系统的健壮性。

  • 迪米特法则要求类“羞涩”一点,尽量不要对外公布太多的public方法和非静态的 public变量,尽量内敛,多使用private、package-private、protected等访问权限。

比如说把大象装冰箱需要三步,任何一步失败都会导致接下来的动作无法执行,我们应该封装一个把大象装冰箱的方法(涵盖这三步)开放出去,而不应该把这三步都开放出去。

  • 迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以 提高。其要求的结果就是产生了大量的中转或跳转类,导致系统的复杂性提高,同时也为维 护带来了难度。读者在采用迪米特法则时需要反复权衡,既做到让结构清晰,又做到高内聚 低耦合。

开闭原则---(开放扩展,关闭修改)

  • 定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

开闭原则的定义已经非常明确地告诉我们:软件实体应该对扩展开放,对修改关闭,其 含义是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化

  • 比如说原来卖书是原件卖,现在突然要打折了,如果改变原有逻辑可能会导致出现各种问题,工作量也大,应该找一种安全的方案小范围改动,比如继承
  • 再比如说,平常工作遇到一个类需要增加个新功能,在原有类的基础上改动一下就可以解决,当时觉着没问题,后来可能就会引发一系列问题,因为这个类已经被很多其他类引用着,直接修改的话非常容易引起一些意想不到的问题。这时候就应该使用扩展方式来实现我们想要的新功能,比如可以新增方法,或者使用继承,这样扩展,既可以保证我们对新需求的实现,又可以避免直接修改方法带来的一系列问题。

推荐阅读更多精彩内容