SilverLining's Blog

七大设计原则

前言

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

七大设计原则:

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

开闭原则

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

定义

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

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

实现方法

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

实现代表:

依赖倒置原则

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

定义

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

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

实现方法

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

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

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

实现代表:

单一职责原则

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

定义

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

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

实现方法

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

接口隔离原则

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

定义

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

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

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

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

也就是说,我们在设计接口的时候有可能满足单一职责原则但是不满足接口隔离原则。

实现方法

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

接口隔离原则的规范:

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

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

定义

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

类与类之间的关系越密切,耦合度就越大。当一个类发生改变时,对另一个类的影响也越大。迪米特法则通俗的来讲,就是一个类对自己依赖的类知道的越少越好。对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的 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) 表示一种强的 “拥有” 关系,一般表现为严格的整体和部分的关系,部分和整体的生命周期是一样的。

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

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

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

实现方法

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

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

实现代表:

一个设计原则的应用案例