七大设计原则

2020-02-23 1868点热度 0人点赞 0条评论

前言

设计原则是对软件工程中常见的设计模式的抽象,可以指导我们设计出更加优雅的代码结构。

七大设计原则:

  • 开放封闭原则
  • 依赖倒置原则
  • 单一职责原则
  • 接口隔离原则
  • 迪米特法则(最少知道原则)
  • 里式替换原则
  • 合成复用原则

但是设计原则并不是解决一切问题的灵丹妙药。在实际开发过程中,并不是一定要求所有代码都遵循设计原则,也要考虑到人力、时间、成本、质量等因素,而不是刻意追求完美。要在适当的场景遵循设计原则,体现的是一种平衡的取舍。

开闭原则

可以对现有代码进行扩展,不应对现有代码修改(影响现有逻辑或功能)

定义

开闭原则 (Open-Closed Principle, OCP) 是指一个软件实体如类、模块和函数应该对扩展开放, 对修改关闭

开闭原则强调的是用抽象构建框架,用实现扩展细节。开闭原则,是面向对象设计中最基础的设计原则,可以提高软件系统的可复用性及可维护性。

实现方法

通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放,其包含三层含义:

  • 通过接口或抽象类约束扩展,对扩展进行边界限定。不允许出现在接口或抽象类中不存在的 public 方法。
  • 参数类型、引用对象尽量使用接口或抽象类,而不是实现类,这主要是实现里氏替换原则的一个要求。
  • 抽象层尽量保持稳定,一旦确定就不要修改。

实现代表:

  • 里氏替换原则(LSP)
  • 依赖倒转原则(DIP)
  • 接口隔离原则(ISP)
  • 抽象类(Abstract Class)
  • 接口 (Interface)
  • 装饰者模式

依赖倒置原则

细节应依赖抽象,而抽象不应依赖细节。应当面向接口编程,而不是面向实现编程。

定义

依赖倒置原则 (Dependence Inversion Principle, DIP) 是指设计代码结构时,

  • 上层模块不应该依赖于下层模块,它们应共同依赖于一个抽象(父类不能依赖子类,它们都要依赖抽象类)
  • 抽象不能依赖于具体,具体应该要依赖于抽象

通过依赖倒置,可以减少类与类之间的耦合性,提高系统的稳定性,提高代码的可读性和可维护性,并能够降低修改程序所造成的风险。

实现方法

  • 每个较高的层次类都为它所需要的服务提出一个接口声明,较低层次类实现这个接口
  • 每个高层类都通过该抽象接口使用服务

在实现依赖倒转原则时,我们需要针对抽象层编程,而将具体类的对象通过依赖注入 (DependencyInjection, DI) 的方式注入到其他对象中。依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。

常用的注入方式有三种,分别是:构造注入,设值注入(Setter 注入)和接口注入。 Spring 的 IOC 是此实现的典范。

从 Java 角度看待依赖倒转原则的本质就是:面向接口(抽象)编程。

  • 每个具体的类都应该有其接口或者基类,或者两者都具备。
  • 类中的引用对象应该是接口或者基类。
  • 任何具体类都不应该派生出子类。
  • 尽量不要覆写基类中的方法。

实现代表:

  • 工厂模式

单一职责原则

避免相同的职责分散到不同的类中;避免一个类承担太多职责。

定义

单一职责 (Simple Responsibility Pinciple,SRP) 是指不要存在多于一个导致类变更的原因。每一个职责都是变化的一个轴线,如果一个类有一个以上的职责,这些职责就耦合在了一起。这会导致脆弱的设计。当一个职责发生变化时,可能会影响其它的职责。另外,多个职责耦合在一起,会影响复用性。

此原则的核心是在顶层设计解耦和增强内聚性。

实现方法

违反 SRP 原则的重构可采取设计模式:

  • 外观模式(Facade)
  • 代理模式(Proxy)
  • 数据访问对象(DAO)
  • 迭代器模式

接口隔离原则

使用多个单一细化的接口,而不是少数庞大臃肿的总接口。

定义

接口隔离原则 (Interface Segregation Principle, ISP) 是指用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口。这个原则指导我们在设计接口时应当注意一下几点:

  • 客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上
  • 建立单一接口,不要建立庞大臃肿的接口
  • 尽量细化接口,接口中的方法尽量少(也不是越少越好,一定要适度)

接口隔离原则符合我们常说的高内聚低耦合的设计思想,从而使得类具有很好的可读性、可扩展性和可维护性。

单一职责原则接口隔离原则

很多人会觉的接口隔离原则单一职责原则很相似,其实不然。

  • 单一职责原则原注重的是类和接口的职责单一,这里职责是从业务逻辑上划分的
  • 接口隔离原则注重的是对接口依赖的隔离。当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。
也就是说,我们在设计接口的时候有可能满足单一职责原则但是不满足接口隔离原则。

实现方法

我们在设计接口的时候,要多花时间去思考,要考虑业务模型,包括以后有可能发生变更的地方还要做一些预判。所以,在进行接口抽象时,对业务模型的理解是非常重要的。

接口隔离原则的规范:

  • 使用接口隔离原则前首先需要满足单一职责原则。
  • 提高接口内聚,减少对外交互(对外发布 public 的方法)。使接口用最少的方法去完成最多的事情。
  • 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性,但是如果拆分过小则会造成接口数量过多,使设计复杂化。所以一定要适度。

迪米特法则(最少知道原则)

一个类对自己依赖的类(的细节)知道的越少越好。

定义

迪米特原则 (Law of Demeter LoD) 是指一个对象应该对其他对象保持最少的了解,又叫最少知道原则 (Least Knowledge Principle, LKP) 。迪米特原则主要强调尽量降低类与类之间的耦合。

类与类之间的关系越密切,耦合度就越大。当一个类发生改变时,对另一个类的影响也越大。迪米特法则通俗的来讲,就是一个类对自己依赖的类知道的越少越好。对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的 public 方法,不对外泄漏任何信息。

实现方法

一个模块或对象应该尽量少地与其他模块或对象发生相互作用,这样当一个模块修改时不会影响太多。

迪米特法则的规则:

  • 只与直接的朋友通讯(Only talk to your immediate friends),一个对象的 “朋友” 包括他本身 (this) 、它持有的成员对象、入参对象、它所创建的对象。
  • 尽量少发布 public 的变量和方法。一旦公开的属性和方法越多,修改的时候影响的范围越大。
  • 是自己的就是自己的。如果一个方法放在本类中,既不产生新的类间依赖,也不造成负面的影响,那么次方法就应该放在本类中。

实现代表:

  • 外观模式
  • 中介者模式

迪米特法则的核心观念就是类间解耦,只有类处于弱耦合状态,类的复用率才会提高。所谓降低类间耦合,实际上就是尽量减少对象之间的交互。

  • 如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用。
  • 如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。

简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。但是这样会引发一个问题,有可能产生大量的中间类或者跳转类,导致系统的复杂性提高,可维护性降低。所以也不能一味追求极度解耦。

里式替换原则

子类可以扩展父类的功能,但不能改变父类原有的功能。

定义

里式替换原则(Liskov Substitution Principle,LSP)的定义是:所有引用基类(父类)的地方必须能透明地使用其子类的对象,也可以简单理解为任何基类可以出现的地方,子类一定可以出现。

由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在程序运行时再确定其子类类型,用子类对象来替换父类对象。

里式替换原则是的缺点是增加了对象之间的耦合性。因此在系统设计时,遵循里氏替换原则,尽量避免子类重写父类的方法,可以有效降低代码出错的可能性。

实现原则

  1. 子类可以实现父类的抽象方法,但是不能覆盖/重写父类的非抽象方法。
  2. 子类中可以增加自己特有的方法。
  3. 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。因为如果子类的参数与父类相同,那么就会变成重写;如果入参类型小于父类,那么父类方法永远不会被执行到。
  4. 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

里式替换原则是实现开闭原则的重要方式之一。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现。所以里氏代换原则是对实现抽象化的具体步骤的规范,同时这些规范也约束了继承的泛滥。

应用案例

这里对上述的第三和第四项原则用代码案例来加以说明。

  1. 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参/入参)要比父类方法的输入参数更宽松。
import java.util.HashMap;
public class Father {
    public void doSomething(HashMap param) {
        System.out.println("Calling Father.doSomething ...");
    }
}

import java.util.Map;
public class Son extends Father{
    public void doSomething(Map param) {    // 方法的形参比父类的更宽松
        System.out.println("Calling Son.doSomething ...");
    }
}

import java.util.HashMap;
public class Client{
    public static void main(String[] args) {
        Father person = new Son();  // 引用基类的地方能透明地使用其子类的对象
        HashMap param = new HashMap();
        f.doSomething(param);
    }
}
# Output: Calling Father.doSomething ...

注意 Son 类的方法前面是不能加 @Override 注解的,因为否则会编译提示报错。因为这并不是重写(Override),而是重载(Overload),因为方法的输入参数类型不同。

  1. 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
import java.util.Map;
public abstract class Father {
    public abstract Map doSomething();
}

import java.util.HashMap;
public class Son extends Father{
    @Override
    public HashMap doSomething() {  // 方法的返回值比父类的更严格
        HashMap result = new HashMap();
        result.put("result", "Calling Son.doSomething ...");
        return result;
    }
}

public class Client{
    public static void main(String[] args) {
        Father person = new Son();  //引用基类的地方能透明地使用其子类的对象。
        System.out.println(person.doSomething());
    }
}
# Output: {result = Calling Son.doSomething ...}

合成复用原则

尽量使用合成/聚合,而不要使用继承.

定义

合成复用原则 (Composite/Aggregate Reuse Principle, CARP) 是指尽量使用对象组合 (has-a)/ 聚合 (contanis-a),而不是继承关系达到软件复用的目的。

聚合 (Aggregate) 表示一种弱的 “拥有” 关系,一般表现为松散的整体和部分的关系。所谓整体和部分也可以是完全不相关的。例如 A 对象持有 B 对象,B 对象并不是 A 对象的一部分,也就是 B 对象的生命周期是 B 对象自身管理,和 A 对象不相关。

合成 (Composite) 表示一种强的 “拥有” 关系,一般表现为严格的整体和部分的关系,部分和整体的生命周期是一样的。

类与类之间的关系可划分为:

  • Is-A 继承关系:表示类与类之间的继承关系、接口与接口之间的继承的关系以及类对接口实现的关系。
  • Has-A 合成关系:是关联关系的一种,是整体和部分(通常为一个私有的变量)之间的关系,并且代表的整体对象负责构建和销毁代表部分对象,代表部分的对象不能共享。
  • Use-A 依赖关系:是类与类(通常为函数的参数)之间的连接,依赖总是单向的。

合成/聚合可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少;其次才考虑继承。

在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度。而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。

实现方法

合成/聚合复用原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向内部持有的这些对象的委派达到复用已有功能的目的,而不是通过继承来获得已有的功能。

只有当以下的条件全部被满足时,才应当使用继承关系。

  • 子类是超类的一个特殊种类,而不是超类的一个角色,也就是区分 “Has-A” 和 “Is-A”. 只有 “Is-A” 关系才符合继承关系,“Has-A” 关系应当使用聚合来描述。
  • 永远不会出现需要将子类换成另外一个类的子类的情况。如果不能肯定将来是否会变成另外一个子类的话,就不要使用继承。
  • 子类具有扩展超类的责任,而不是具有置换掉或注销掉超类的责任。如果一个子类需要大量的置换掉超类的行为,那么这个类就不应该是这个超类的子类。

实现代表:

  • 策略模式

一个设计原则的应用案例

design_principle_sample

SilverLining

也可能是只程序猿

文章评论