跳至主要內容

常见设计原则

AruNi_Lu设计模式设计原则与思想约 6699 字大约 22 分钟

本文内容

1. 设计原则概览

在进行面向对象编程时,常常需要和设计原则一起来进行系统开发,以让我们的系统更具扩展性和可维护性。

常见的设计原则有五种,它们被合称为 SOLID,分别表示的原则如下表:

此外,还有 DRY 原则、KISS 原则、YAGNI 原则、LOD 法则,留到下一篇文章介绍。本文主要介绍 SOLID 五大设计原则。

其实,这些设计原则理解起来都不难,但是想要灵活应用,在实际编码过程中体现出来,是需要有一定实践经验和积累的。

2. SRP 单一职责原则

SRP(Single Responsibility Principle)称为 单一职责原则,意思是 一个类或模块只负责完成一个职责(或功能)。也就是设计的 粒度要小、功能单一,而不要设计大而全的类。

在不同的应用场景、不同阶段的需求背景下,对同一个类的职责是否单一的判定,可能都是不一样的。

例如是要把地址信息 AddressInfo 写在用户信息类 UserInfo 中,还是单独写一个 Address 类?

如果地址信息和其他信息一样,只是用来单纯的展示,那么将 AddressInfo 写到 UserInfo 中,也是满足单一职责原则的。但是如果项目又开发了一个电商模块,那么 AddressInfo 还会用在电商物流中,此时就需要将它单独抽取出来了。

所以在实际开发时,可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这时就可以将这个粗粒度的类,拆分成几个更细粒度的类。这就是所谓的 持续重构

但是,也 不是要把类的职责设计得越单一越好,否则会影响类的内聚性

例如一个 Serializer 类,负责序列化和反序列化,如果把它拆分为 Serializer 类和 Deserializer 类,如果要修改序列化协议的格式或者反序列化输出的结果格式时,就需要在两个类中修改,内聚性没有一个类时高。如果只修改一个类而忘记修改了另一个类,那直接就会导致运行出错,是的代码的可维护性变差了。

单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性

在实际开发中,特别是 Constants 类和 Utils 类,很容易写出违背单一职责原则的代码,此时就需要合理拆分了。

3. OCP 开闭原则

OCP(Open Closed Principal)称为 开闭原则,意思是 软件实体(模块/类/方法等)应该 "对扩展开放,对修改关闭"。也就是 当新增一个功能时,是在已有代码的基础上扩展代码,而不是修改已有代码

需要注意的是,这里的扩展和修改是相对而言的,并没有绝对的说法。例如在一个类中新增某个方法,对于其他方法来说,这属于扩展,而对于这个类来说,这个新增也是对这个类的修改。

所以说开闭原则并不是说要完全杜绝修改,当然也做不到,而是以最小的修改代码代价来完成新功能的开发,这就是合理的。

想要做到开闭原则,就要时刻具备扩展意识、抽象意识、封装意识。在编写代码时,多考虑哪些需求代码的修改性比较大,哪些地方需要留扩展点。

最长用来 提高代码扩展性 的方法有:多态、基于接口而非实现编程、依赖注入和设计模式等

4. LSP 里氏替换原则

LSP(Liskov Substitution Principle)称为 里氏替换原则,意思是 子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变和正确性不被破坏。也就是 子类需要完美继承父类的设计初衷,并做了增强

下面通过一个例子来具体理解下。父类 Transporter 使用 HttpClient 类来传输网络数据。子类 SecurityTransporter 继承即父类,增加了额外功能—传输 appId 和 appToken 来进行安全认证。

Transporter 父类:

public class Transporter {
  private HttpClient httpClient;
  
  public Transporter(HttpClient httpClient) {
    this.httpClient = httpClient;
  }

  public Response sendRequest(Request request) {
    // ...use httpClient to send request
  }
}

SecurityTransporter 子类:

public class SecurityTransporter extends Transporter {
  private String appId;
  private String appToken;

  public SecurityTransporter(HttpClient httpClient, String appId, String appToken) {
    super(httpClient);
    this.appId = appId;
    this.appToken = appToken;
  }

  @Override
  public Response sendRequest(Request request) {
    // 如果有 appId 和 appToken,则添加进 request
    if (StringUtils.isNotBlank(appId) && StringUtils.isNotBlank(appToken)) {
      request.addPayload("app-id", appId);
      request.addPayload("app-token", appToken);
    }
    return super.sendRequest(request);
  }
}

测试类:

public class Demo {    
  // 此方法接收的参数为Transporter
  public void demoFunction(Transporter transporter) {    
    Reuqest request = new Request();
    //...省略设置request中数据值的代码...
    Response response = transporter.sendRequest(request);
    //...省略其他逻辑...
  }
}

// 里式替换原则
Demo demo = new Demo();
demo.demofunction(new SecurityTransporter(/*省略参数*/););

可以看见,子类 SecurityTransporter 可以替换父类出现的任何位置,并且原来代码的逻辑行为不变和正确性没被破坏,是 符合里氏替换原则的

这么一看氏替换原则好像和面向对象的多态特性一样,但实际上它们完全是两回事。

4.1 LSP 和多态的区别

继续来看上面的例子,现在我们对 SecurityTransporter 中的 sendRequest() 函数稍微修改一下:

// 改造前:
public class SecurityTransporter extends Transporter {
  //...省略其他代码..
  @Override
  public Response sendRequest(Request request) {
    // 如果有 appId 和 appToken,则添加进 request
    if (StringUtils.isNotBlank(appId) && StringUtils.isNotBlank(appToken)) {
      request.addPayload("app-id", appId);
      request.addPayload("app-token", appToken);
    }
    return super.sendRequest(request);
  }
}

// 改造后:
public class SecurityTransporter extends Transporter {
  //...省略其他代码..
  @Override
  public Response sendRequest(Request request) {
    // 如果没有传入 appId 或 appToken,则抛异常
    if (StringUtils.isBlank(appId) || StringUtils.isBlank(appToken)) {
      throw new NoAuthorizationRuntimeException(...);
    }
    request.addPayload("app-id", appId);
    request.addPayload("app-token", appToken);
    return super.sendRequest(request);
  }
}

在改造之后的代码中,如果传递进 demoFunction() 函数的是父类 Transporter 对象,那 demoFunction() 函数并不会有异常抛出,但如果传递给 demoFunction() 函数的是子类 SecurityTransporter 对象,那 demoFunction() 有可能会有异常抛出(如果没有对 appId/appToken 赋值)。尽管代码中抛出的是运行时异常(Runtime Exception),我们可以不在代码中显式地捕获处理,但子类替换父类传递进 demoFunction 函数之后,整个程序的逻辑行为有了改变

所以,改造后的代码虽然仍符合多态特性,但却不满足里氏替换原则了。里氏替换原则一定要注重看后半部分,即对逻辑行为和正确性的影响。

因此,虽然从描述上来看多态和里氏替换有点类似,但是 它们关注的角度是不一样的

  • 多态是一种面向对象编程的特性,是一种 代码实现思路
  • 历史替换是一种设计原则,用来 指导继承关系中子类应该如何设计,要保证子类在替换父类时,不改变原有程序的逻辑行为以及不破坏原来程序的正确性。

4.2 更加通俗地理解 LSP

实际上,里氏替换原则还有另一个更加能落地、更具有指导意义的描述,就是 "Design By Contract",也就是 "按照协议来设计"。

这个协议,其实就是 子类在设计时,需要遵守父类的行为约定(协议)。父类定义了函数的行为约定,子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定

实际上,父类和子类也可以替换成接口和实现类。

这个行为约定,可以是:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括函数注释中的特殊说明

4.3 哪些代码设计明显违背了 LSP?

下面列举一些 违背了里氏替换原则 的代码设计:

  • 子类违背父类声明要实现的功能

    • 比如父类提供的函数 sortOrdersByAmount() 是按照金额从小到大对订单进行排序。而子类重写此函数时按照创建日期来给订单排序;
  • 子类违背父类的输入、输出、异常的约定

    • 在父类中某个函数规定运行出错时返回 null,获取数据为空时返回空集合({})。而子类重写此函数后,变成了运行出错抛出异常,获取不到数据返回 null;
    • 在父类中某个函数规定输入数据可以是任意整数。而子类实现时却只允许输入是正整数,负数就抛异常;
    • 在父类中某个函数规定只会抛出 ArgumentNullException。而子类实现中还抛出了其他异常。
  • 子类违背父类注释中的特殊说明

    • 父类中定义的 withdraw() 提现功能的注释是 "用户的提现金额不得超过账户余额..."。而子类的实现中针对 VIP 用户提供了透支提现的功能,也就是提现金额可以大于账户余额。

另外,判断子类的设计是否违背里氏替换原则,其实一个很好的方法就是 拿父类的单元测试去验证子类的代码。如果某些单元测试运行失败,则子类有可能没有遵守父类的约定,子类就可能违背了里氏替换原则。

所以说,里氏替换原则就是子类要完美继承父类的设计初衷,并做了增强

这里的增强,就好比上面的例子中,子类 SecurityTransporter 增加了一个安全认证,其实就是对父类 Transporter 的增强。

5. ISP 接口隔离原则

ISP(Interface Segregation Principle)称为 接口隔离原则,意思是 接口的调用者不应该被强迫依赖它不需要的接口

在 ISP 中,我们可以把其中的 "接口" 理解为下面三种东西:

  • 一组 API 接口集合
  • 单个 API 接口或函数
  • OOP 中的接口概念

下面就按照这三种理解展开对 ISP 原则的学习。

5.1 把 "接口" 理解为一组 API 接口集合

例如,微服务用户系统提供了一组跟用户相关的 API 给其他系统使用,其中有注册、登录、查询用户信息等功能。代码如下:

public interface UserService {
  boolean register(String cellphone, String password);
  boolean login(String cellphone, String password);
  UserInfo getUserInfoById(long id);
  UserInfo getUserInfoByCellphone(String cellphone);
}

public class UserServiceImpl implements UserService {
  //...
}

如果后台管理系统要实现删除用户的功能,希望上面这个用户系统提供一个删除用户的接口。这时候怎么做?最直接的办法就是直接在 UserService 中添加 deleteUserById()deleteUserByCellphone() 接口即可,但是这样会留下安全隐患。

删除用户是非常慎重的操作,我们应该只给后台管理系统使用,所以如果放到 UserService 中,那所有使用 UserService 的系统,都可以调用此接口,是十分危险的。

当然了,最好的解决方案是通过接口鉴权来限制接口的调用。不过我们也可以从代码设计的层面来避免接口不被误用。

根据接口隔离原则,调用者不应该强迫依赖它不需要的接口。所以我们可以将删除接口单独设计出一个接口 RestrictedUserService,此接口只打包提供给后台管理系统来使用,其他系统就使用不到了。就像下面这样:

public interface UserService {
  boolean register(String cellphone, String password);
  boolean login(String cellphone, String password);
  UserInfo getUserInfoById(long id);
  UserInfo getUserInfoByCellphone(String cellphone);
}

public interface RestrictedUserService {
  boolean deleteUserByCellphone(String cellphone);
  boolean deleteUserById(long id);
}

// 实现两个接口
public class UserServiceImpl implements UserService, RestrictedUserService {
  // ...省略实现代码...
}

这样设计,就符合接口隔离原则了。所以,我们在设计微服务或者类库的一组接口时,如果部分接口只被部分调用者使用,那就需要将这部分接口隔离出来,单独给对应的调用者使用,而不是强迫其他调用者也依赖于这部分不被使用到的接口

5.2 把 "接口" 理解为单个 API 接口或函数

当把接口理解为单个接口或函数时,意思就是 接口或函数的设计要功能单一,不要把多个不同的功能逻辑在一个接口或函数中实现

这样一看,好像接口隔离原则跟单一职责原则有点类似,但是还有有点 区别 的:

  • 单一职责原则针对的是 模块、类、接口 的设计;
  • 接口隔离原则更侧重 接口 的设计,而且它 提供了一种判断接口是否职责单一的标准:通过调用者如何使用接口来间接判断,即 如果调用者只使用部分接口或接口的部分功能,那这个接口的设计就不够职责单一

5.3 把 "接口" 理解为 OOP 中的接口

还可以把接口理解为 OOP 中的接口概念,例如 Java 中的 interface。

例如项目中使用到了三个外部系统:MySQL、Redis、Kafka,每个系统都对应了一系列配置信息,比如地址、端口、访问超时时间等。为了供项目中的其他模块使用这些配置,我们设计出三个 Configuration 类:MySQLConfig、RedisConfig、KafkaConfig。

接下来有两个新功能:

  • 希望 Redis 和 Kafka 支持热更新功能,而 MySQL 不支持;
  • 希望 MySQL 和 Redis 能够暴露配置信息到一个固定的 HTTP 地址(比如 http://127.0.0.1:2389/configopen in new window ),方便查看,而不希望暴露 Kafka 的配置信息。

为了实现这两个新功能,一个简单的做法就是设计一个 Config 接口,提供热更新功能的方法 update()(热更新的具体实现就是定时任务去调用这个 update() 方法)和暴露配置信息的方法 output()(暴露配置信息的具体实现就是通过简单的 HTTP 路由组件),让三个配置类都去实现这两个方法就行了。但是这样设计其实做了一些 无用功,即 MySQL 也必须实现 update() 方法,Kafka 也必须实现 output() 方法。

另一种更好的设计是,分别设计两个接口,Updater 和 Viewer,在这两个接口中分别提供各自的方法。那么配置类具有什么功能,就去实现对应的接口即可。

而且这样的设计还 更加灵活、易扩展、易复用,因为 Updater 和 Viewer 的职责更加单一,单一就意味着通用、复用性好。比如又有一个新模块,需要统计性能,也希望它能够显示在网页上,此时就可以实现 Viewer 接口,从而复用之前的 HTTP 路由组件。

6. DIP 依赖反转原则

在讲解依赖反转原则前,先来看看一些比较容易混淆的概念,例如控制反转(IOC)、依赖注入(DI)、依赖注入框架(DI Framework)。

6.1 控制反转 IOC

IOC(Inversion Of Control)称为 控制反转。注意,这里的 IOC 并不是指 Spring 的 IOC,暂时先不要把这两者联系在一起。

下面通过一个例子,来看看什么是控制反转。

public class UserServiceTest {
  public static boolean doTest() {
    // ... 
  }
  
  public static void main(String[] args) {	// 这部分逻辑可以放到框架中
    if (doTest()) {
      System.out.println("Test succeed.");
    } else {
      System.out.println("Test failed.");
    }
  }
}

在上面的代码中,所有流程都由程序员来控制。其实我们可以抽象出一个框架,让框架来做这些流程控制。就像下面这样:

// TestCase 抽象类
public abstract class TestCase {
  // 提供一个 run() 方法,实现流程的控制
  public void run() {
    if (doTest()) {
      System.out.println("Test succeed.");
    } else {
      System.out.println("Test failed.");
    }
  }
  
  // 扩展点:提供一个 doTest() 抽象方法,交给子类重写实现具体的逻辑
  public abstract boolean doTest();
}

public class JunitApplication {
  private static final List<TestCase> testCases = new ArrayList<>();
  
  // 将需要测试的 testCase 注册进来
  public static void register(TestCase testCase) {
    testCases.add(testCase);
  }
  
  public static final void main(String[] args) {
    for (TestCase case: testCases) {
      case.run();
    }
  }
}

在上面的测试框架中,预留了一个扩展点,也就是 TestCase 抽象类中的 doTest() 抽象方法。那么哪个类如果需要测试,直接继承 TestCase,重写 doTest() 方法,然后将该类对象通过 JunitApplication 类的 register(TestCase testCase) 注册进测试框架即可。例如:

public class UserServiceTest extends TestCase {
  @Override
  public boolean doTest() {
    // ... 
  }
}

// 注册操作还可以通过配置的方式来实现,不需要程序员显示调用 register()
JunitApplication.register(new UserServiceTest();

上面就是一个典型的通过框架来实现 "控制反转" 的例子。框架提供了一个可扩展的代码骨架,用来组装对象、管理整个执行流程(run() 方法中的实现)。程序员在利用框架开发时,只需要往预约的扩展点上(doTest() 抽象方法),添加跟自己业务相关的代码(重写 doTest(),实现自己的业务逻辑),就可以利用框架来驱动整个程序流程的执行

这里的 “控制” 指的是对程序执行流程的控制,而 “反转” 指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程可以通过框架来控制。流程的控制权从程序员 “反转” 到了框架。

其实上面那种 类似于模板方法设计模式 的设计,只是实现控制反转的一种方法,还有 依赖注入 等方法,也能实现控制反转。所以控制反转只是一个 笼统的设计思想,并不是一种具体的实现技巧。

6.2 依赖注入 DI

DI(Dependency Injection)称为 依赖注入,与控制反转恰恰相反,它是一种 具体的编码技巧

依赖注入 用一句话来概括就是:不通过 new() 的方式在类的内部创建依赖的对象,而是将依赖的对象在外部创建好,通过构造器、函数参数、setter 方法等方式传递(注入)给该类使用

还是举个例子,来看看非依赖注入和依赖注入方式的区别。

下面的例子中,Notification 类负责消息推送,依赖 MessageSender 类实现推送商品促销、验证码等消息给用户。

非依赖注入实现:

public class Notification {
  // 依赖 MessageSender
  private MessageSender messageSender;
  
  public Notification() {
    // 直接 new(),此处有点像 hardcode
    this.messageSender = new MessageSender(); 	
  }
  
  public void sendMessage(String cellphone, String message) {
    //...省略校验逻辑等...
    this.messageSender.send(cellphone, message);
  }
}
//--------------------------------------------------------------------------------------
public class MessageSender {
  public void send(String cellphone, String message) {
    //....
  }
}

// 使用 Notification
Notification notification = new Notification();

依赖注入实现:

public class Notification {
  // 依赖 MessageSender
  private MessageSender messageSender;
  
  // 内部通过构造函数将依赖的 messageSender 传递进来使用
  public Notification(MessageSender messageSender) {
    this.messageSender = messageSender;
  }
  
  public void sendMessage(String cellphone, String message) {
    //...省略校验逻辑等...
    this.messageSender.send(cellphone, message);
  }
}

// 使用 Notification
MessageSender messageSender = new MessageSender();	// 在外部 new MessageSender 
Notification notification = new Notification(messageSender);    // 将依赖通过构造器传入

可以发现,通过 依赖注入的方式将依赖的类对象传递进来,可以提高代码的扩展性,因为可以灵活地替换依赖的类(只要是子类或实现类,都可以传递进去)。

其实,上面的代码还可以继续优化,将 MessageSender 定义成接口,改造后的代码如下:

public class Notification {
  private MessageSender messageSender;
  
  public Notification(MessageSender messageSender) {
    this.messageSender = messageSender;
  }
  
  public void sendMessage(String cellphone, String message) {
    this.messageSender.send(cellphone, message);
  }
}
//--------------------------MessageSender 接口-------------------------------
public interface MessageSender {
  void send(String cellphone, String message);
}

// -----------------------------------短信发送类----------------------------------
public class SmsSender implements MessageSender {
  @Override
  public void send(String cellphone, String message) {
    //....
  }
}

// -----------------------------------站内信发送类------------------------------
public class InboxSender implements MessageSender {
  @Override
  public void send(String cellphone, String message) {
    //....
  }
}

// 使用 Notification
MessageSender messageSender = new SmsSender();
Notification notification = new Notification(messageSender);

这样,就可以灵活地将不同的实现类传递给 Notification 了。

这就是依赖注入,非常简单,但却非常有用。使用函数参数、setter 和构造器的方式类似。

6.3 依赖注入框架 DI Framework

知道了什么是依赖注入后,再来看看什么是依赖注入框架。还是借助上面的例子来讲解。

在上面的依赖注入实现方式中,虽然我们不用在类的内部通过 new 来创建 MessageSender 对象。但是这个创建对象、组装(注入)对象的工作仅仅是被移到了更上层而已,还是需要我们程序员自己来实现,就像下面这样:

public class Demo {
  public static final void main(String args[]) {
    MessageSender sender = new SmsSender(); 	// 创建对象
    Notification notification = new Notification(sender);	// 依赖注入
    notification.sendMessage("13918942177", "短信验证码:2346");
  }
}

在实际开发中,一个项目有几十、几百个类,这会使类对象的创建和依赖注入变得非常复杂。而对象创建和依赖注入又和业务无关,所以我们就需要抽象出一个框架来自动完成这项复杂的工作。

这个框架就是 "依赖注入框架"。我们只需要 通过依赖注入框架提供的扩展点,简单地配置一下所有需要创建地类对象、类与类之间的依赖关系,就可以 由框架来自动创建对象、管理对象的声明周期、依赖注入 等繁琐的事情。

现成的依赖注入框架有很多,比如 Google Guice、Java Spring 等。

Spring 框架的 IOC 主要就是使用依赖注入的方式来实现的。另外还有之前我们编写的 模板方法模式,也能实现 IOC。

Spring 提供了一些简单的配置来创建类对象、表明类与类之间的依赖关系,比如 spring.xml 配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans>
    <!-- 把 UserDao 这个类交给 Spring 框架管理 -->
    <bean id="userDao" class="com.run.test.bean.UserDao"/>

    <!-- 把 UserService 这个类交给 Spring 框架管理 -->
    <bean id="userService" class="com.run.test.bean.UserService">
        <!-- UserService 类所依赖得属性  -->
        <property name="uId" value="10001"/>
        <property name="company" value="腾讯"/>
        <property name="location" value="深圳"/>
        <property name="userDao" ref="userDao"/>
    </bean>
</beans>

还有 SpringBoot 提供的更为方便的注解方式:

@Service	// 把 UserService 交给 Spring 框架管理
public class UserService {
    
    @Autowire	// 通过注解进行依赖注入
    private UserDao userDao;
    
    public findUserById(int userId) {
        // 直接使用
        User user = userDao.selectUserById(userId);
        // ......
    }
}

@Repository		// 把 UserDao 交给 Spring 框架管理
public class UserDao {
    // ......
}

这就是依赖注入框架的魅力,极大的提高了开发效率,让我们 不再去关注类对象的创建与依赖关系,直接在需要类对象的时候拿来使用即可,把这些琐碎的事情交给了框架来做。

6.4 依赖反转原则 DIP

前面铺垫了这么多,终于轮到主角 DIP 登场了。

DIP(Dependency Inversion Principle)称为 依赖反转原则,或者依赖倒置原则。意思是 高层模块不要依赖低层模块。高层模块和低层模块应该通过抽象来互相依赖。除此之外,抽象不要依赖具体实现细节,具体实现细节依赖抽象

这概念确实抽象,不过你可以重复的多品读几遍,读完之后,你就会发现,还是很抽象。哈哈哈,下面就来具体的分析一下这段描述。

其中的高层模块和低层模块,简单理解就是:在调用链上,调用者属于高层,被调用者属于低层

在我们平时的 业务开发中,高层模块依赖底层模块是没有任何问题的,这条原则主要还是用来 指导框架层面的设计,跟 IOC 类似。

一个符合依赖反转原则的典型例子是 Tomcat 和 Servlet 容器。Tomcat 是运行 Java Web 应用程序的容器,我们编写的 Web 应用程序代码只需要部署在 Tomcat 容器下,便可以被 Tomcat 容器调用执行。此时,Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个 "抽象",也就是 Servlet 规范Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范

具体来说,Tomcat 是一个 Web 服务器,其内部规定好了如何处理 HTTP 请求报文,它会将请求报文转换成符合 Servlet 规范的 ServletRequest,然后传递到我们的应用程序代码。也可以将我们编写的符合 Servlet 规范的 ServletResponse 转换为 HTTP 响应报文,返回给客户端。所以Tomcat 需要依赖 Servlet 规范,我们编写的代码也需要依赖 Servlet 规范,Tomcat 和我们编写的应用程序代码才能通过 "Servlet 抽象" 进行交互。

再举个例子,JVM 和 Java 代码。JVM 是运行 Java 代码的机器,此时,JVM 是高层模块,Java 代码是低层模块。JVM 和 Java 代码之间并没有直接的依赖关系,两者都依赖同一个抽象,也就是 class 字节码。字节码不依赖具体的 JVM 和 Java 代码,而 JVM 和 Java 代码依赖于字节码规范。

这样的好处在于 将 JVM 于 Java 代码高度解耦,JVM 可以有不同的实现(Hotspot、ART 等),Java 代码也可以替换成 Groovy、Kotlin 等,只要它们符合字节码规范即可

所以,字节码不仅屏蔽了平台之间的差异(只要在各平台安装对应的 JVM 即可),而且还解除了 JVM 和 Java 代码之间的耦合

所以依赖反转原则中的反转就是指:把两个相互依赖的高层模块和低层模块,反转给中间的抽象层来实现,让高低层都指向这个中间层,而不是真正的互相依赖

7. 总结

一张图总结上面的五大设计原则:

上次编辑于: