先吹响口号_6大设计原则

引:先有6大设计原则,后有23种设计模式。让我们先吹响这先行的口号。

单一职责原则 SRP

定义

就一个类或接口而言,应该有且只有一个原因引起类的变更。

例子

关于电话通话的接口,有三个过程:拨号、通话、挂机。代码如下:

1
2
3
4
5
6
7
8
public interface IPhone {
//拨通电话
public void dial(String phoneNumber);
//通话
public void chat(Onbject o);
//挂机
public void hangup();
}

但是IPhone这个接口不是只有一个职责,它包含了两个职责:一个是协议管理,一个是数据传送。所以我们改为下面设计的类图:
srp

好处

  • 类的复杂性降低,实现什么职责都有清晰明确的定义
  • 可读性提高,复杂度降低,那当然可读性提高了
  • 可维护性提高,可读性提高了,那当然更容易维护了
  • 变更引起的风险降低

最佳实践

接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。

里氏替换原则 LSP

定义

  1. 正宗定义:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o2都代换成o1时,程序P的行为没有发生变化,那么类型S是类型T的子类型。(感觉《Java设计模式之禅》的翻译错误)
  2. 通俗定义:所有引用基类的地方必须能透明地使用其子类的对象(只要父类能出现得地方子类就可以出现;有子类出现的地方,父类未必能适应)。

规则

  1. 子类必须完全实现父类的方法。(如果子类不能完整地实现子类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚合、组合灯关系来代替继承。)
  2. 子类可以有自己的个性。
  3. 覆盖或实现父类的方法时输入参数可以被放大。(子类中方法的前置条件(方法中输入参数)必须与超类中被覆写的方法的前置条件相同或者更宽松。)
  4. 覆写或实现父类的方法时输出结果可以被缩小。

疑问(多态是否违背LSP?)

如果继承的目的是为了多态,而多态的前提就是子类覆盖并重新定义父类的方法,为了符合LSP,我们应该将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法,当父类是抽象类时,父类就是不能实例化,所以也不存在可实例化的父类对象在程序里。也就不存在子类替换父类实例时逻辑不一致的可能。

最佳实践

在项目中,采用里氏替换原则时,尽量避免子类的“个性”,一旦子类有了“个性”,这个子类和父类之间的关系就难调和,把子类当做父类使用,子类的“个性”被抺杀了,把子类单独作为一个业务来使用,则会让代码间的耦合关系变得扑朔迷离–缺乏类替换的标准。

依赖倒置原则 DIP

定义

高层模块不应该依赖底层模块,两者都应该依赖其抽象。抽象不应该依赖细节。细节应该依赖抽象。(不可以分割的原子逻辑就是底层模块,原子逻辑的再组装就是高层模块。抽象就是指接口或抽象类。细节就是实现类。)

Java语言表现:模块间的依赖通过抽象产生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。接口或抽象类不依赖实现类。实现类依赖接口或抽象类。——面向接口编程(OOD)

依赖的三种写法

  1. 构造函数传递依赖对象(构造函数注入)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public interface IDriver {
    public void drive();
    }

    public class Driver implements IDriver {
    private ICar car;
    //构造函数注入
    public Drive(ICar _car) {
    this.car = _car;
    }

    public void drive() {
    this.car.run();
    }
    }
  2. Setter方法传递依赖对象(Setter依赖注入)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public interface IDriver {
    public void setCar(ICar car);
    public void drive();
    }

    public class Driver implements IDriver {
    private ICar car;
    //setter注入
    public void setCar(ICar car) {
    this.car = car;
    }

    public void drive() {
    this.car.run();
    }
    }
  3. 接口声明依赖对象(接口注入)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public interface IDriver {
    public void drive(ICar car);
    }

    public class Driver implements IDriver {
    //接口注入
    public void drive(ICar car) {
    car.run();
    }
    }

最佳实践

  1. 每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备。
  2. 变量的表面类型(定义的类型)尽量是接口或者是抽象类。
  3. 任何类都不应该从具体类派生。
  4. 尽量不要覆写基类的方法。
  5. 结合里氏替换原则使用(多态)

我们在实际的项目中使用依赖倒置原则需要审时度势,不哟啊抓住一个原则不放,每一个原则的优点都是有限度的,并不是放之四海而皆准的真理,所以别为了遵循一个原则而放弃了一个项目的终极目标:投产上线和盈利。

接口隔离原则 ISP

定义

客户端不应该依赖他不需要的接口。类间的依赖关系应该建立在最小的接口上。
(实例接口:class;类接口:interface)

约束

  1. 接口要尽量小。(根据接口隔离原则拆分接口时,首先必须要满足单一职责原则。)
  2. 接口要高内聚。(在接口中尽量少公布public方法。)
  3. 定制服务。(设计时需要为各个访问者定制服务(接口)。)
  4. 接口设计时有限度的。(接口的设计粒度要协调开发难度和可维护性。)

最佳实践

  1. 一个接口只服务于一个子模块或业务逻辑。
  2. 通过业务逻辑压缩接口中的public方法,经常回顾接口。
  3. 已经被污染的接口,尽量去修改,如果变更风险较大,则采用适配器模式进行转化处理。
  4. 了解环境,拒绝盲从。

迪米特法则 LoD LKP

定义

一个对象应该对其他对象有最少的了解。(类解耦)

含义

  1. 只与朋友交流

    朋友类的定义:出现在成员变量、方法的输入输出参数的类称为成员朋友类。

    所以在原类的方法中不能出现非朋友的类,JDK API提供的类除外。

  2. 朋友间也是有距离的

    尽量不要对外公布太多的public方法和非静态的public变量。

  3. 是自己的就是自己的

    如果过一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中。

  4. 谨慎使用Serializable

    防止客户端和服务器端类不同步。

最佳实践

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

开闭原则 OCP

定义

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

PS: 3W原则(what:是什么;why:为什么;How:怎么做)

是什么

开闭原则告诉我们应该尽量通过扩展软件实体来实现变化,而不是通过修改已有的代码来完成变化,它是为软件实体的未来事件而制定的对现行开发设计进行的约束规则。

开闭原则对扩展开放,对修改关闭,并不以为着不做任何修改,低层模块的变更,必然要有高层模块的进行耦合,否则就是一个孤立无意义的代码片段。

一个项目的基本路径:项目开发、重构、测试、投产、运维,其中的重构可以对原有的设计和代码进行修改,运维尽量减少对原有代码的修改,保持历史代码的纯洁性,提高系统的稳定性。

为什么

  1. 简化测试:如果改变软件内容, 需要将所有的测试流程都执行一遍, 如 单元测试, 功能测试, 集成测试等, 如果只是扩展, 只单独测试扩展部分即可。
  2. 提高复用性:所有逻辑都从原子逻辑组合, 原子逻辑粒度越小, 复用性越大; 这样避免相同逻辑存在, 修改时需要修改多个此相同逻辑。
  3. 提高可维护性:维护一个类最好的方式是扩展一个类, 而不是修改一个类, 如果需要修改需要读懂源码才能修改, 扩展的话只需要了解即可, 直接继承扩展。

怎么做

  1. 抽象约束

    通过接口或抽象类可以约束一组可能变化的行为,并且实现对扩展开放。其中包括三层含义:第一,通过接口或抽象类约束扩展,对扩展进行边界设定,不允许出现在接口或抽象类不存在的public方法;第二,参数类型,引用对象尽量使用接口或者抽象类,而不是实现类;第三,抽象层尽量保持稳定。

  2. 元数据控制模块行为

    通过配置参数(从文件或者数据库中来)来控制行为,例如spring配置文件的构造函数注入配置。

  3. 制定项目章程

    约定优于配置。

  4. 封装变化

    对变化的封装包含两层含义:第一,将相同的变化封装到一个接口或抽象类中;第二,将不同的变化封装到不同的接口或抽象类中。23个设计模式都是从各个不同的角度对变化进行封装的。

最佳实践

  1. 开闭原则只是一个原则,适当时候也可以进行补充。
  2. 项目规章非常重要。
  3. 预知变化,项目需要具有可扩展性。

总结

把上面6大原则的首字母(里氏替换原则和迪米特法则的首字母重复,只取一个)联合起来就是SOLID(稳定的),其代码的含义就是把这6个原则结合使用的好处:建立稳定灵活,健壮的设计,而开闭原则又是重中之中,是最基础的原则,是其他5大原则的精神领袖。

参考

  1. 《设计模式之禅》