06.依赖倒置原则介绍

简介: 依赖倒置原则是面向对象设计六大原则之一,强调高层模块不应依赖低层模块,两者应依赖于抽象。通过依赖接口或抽象类而非具体实现,降低模块耦合度,提升系统灵活性和可维护性。本文详解该原则的概念、目标、思想及其实现方式(如依赖注入),并结合多数据库操作、用户购买家电、发送消息等实际案例,深入探讨其应用与优缺点。

06.依赖倒置原则介绍

目录介绍

  • 01.问题思考的分析
  • 02.学习依赖倒置目标
  • 03.理解依赖倒置原则
  • 04.依赖倒置原则思想
  • 05.多数据库操作案例
  • 06.用户购买家电案例
  • 07.发送消息的案例
  • 08.依赖倒置VS依赖注入
  • 09.依赖倒置的思考
  • 10.依赖倒置原则总结

01.问题思考的分析

依赖倒置原则。单一职责原则和开闭原则的原理比较简单,但是,想要在实践中用好却比较难。而今天要讲到的依赖倒置原则正好相反。

这个原则用起来比较简单,但概念理解起来比较难。比如,下面这几个问题,你看看能否清晰地回答出来:

  1. “依赖倒置”这个概念指的是“谁跟谁”的“什么依赖”被反转了?“倒置”两个字该如何理解?
  2. 经常听到另外一个概念:依赖注入。这两个概念跟“依赖倒置”有什么区别和联系呢?它们说的是同一个事情吗?

02.学习依赖倒置目标

  1. 搞懂依赖倒置原则,为什么依赖倒置是重要的,以及它如何帮助我们构建灵活、可扩展和可维护的软件系统。
  2. 掌握依赖倒置的实现方式,依赖注入、工厂模式、策略模式等常见的实现方式,以及它们的优缺点和适用场景。
  3. 应用依赖倒置到实际项目:通过实际项目的练习和实践,将依赖倒置应用到实际的软件开发中。

03.理解依赖倒置原则

依赖倒置原则是面向对象设计的六大原则之一,它的定义是:

  1. 高层模块不应该依赖于低层模块。两者都应该依赖于抽象。
  2. 抽象不应该依赖于细节。细节应该依赖于抽象。

换句话说,设计中应当依赖于接口或抽象类,而不是依赖于具体实现。这种设计有助于减少模块之间的耦合,增加系统的灵活性和可维护性。

04.依赖倒置原则思想

依赖倒置原则的核心思想是面向抽象编程而不是面向具体编程。通过依赖抽象层次(如接口、抽象类),可以让高层模块不依赖具体的实现细节,从而使得系统更具扩展性和灵活性。

05.多数据库操作案例

例子1:违反依赖倒置原则。假设我们有一个UserService类,它直接依赖于MySQLDatabase类来进行数据操作。

class MySQLDatabase {
   
    public void saveUser(String username) {
   
        // 保存用户到MySQL数据库
        System.out.println("Saving " + username + " to MySQL database.");
    }
}

class UserService {
   
    private MySQLDatabase database = new MySQLDatabase();

    public void addUser(String username) {
   
        database.saveUser(username);
    }
}

在这个例子中,UserService类直接依赖于MySQLDatabase类,这是对具体实现的依赖,违反了依赖倒置原则。

如果我们需要将数据库换成PostgresSQLDatabase,或者换成AliSQLDatabase,我们必须修改UserService类,这增加了系统的耦合度和修改的风险。

例子2:遵循依赖倒置原则。可以通过引入一个抽象层来遵循依赖倒置原则。

// 抽象层:定义一个接口,供不同的数据库实现
interface Database {
   
    void saveUser(String username);
}

// 具体实现:MySQL数据库
class MySQLDatabase implements Database {
   
    public void saveUser(String username) {
   
        System.out.println("Saving " + username + " to MySQL database.");
    }
}

// 具体实现:PostgreSQL数据库
class PostgreSQLDatabase implements Database {
   
    public void saveUser(String username) {
   
        System.out.println("Saving " + username + " to PostgreSQL database.");
    }
}

// 高层模块:UserService类依赖于抽象接口,而不是具体实现
class UserService {
   

    private Database database;

    public UserService(Database database) {
   
        this.database = database;
    }

    public void addUser(String username) {
   
        database.saveUser(username);
    }
}

在这个例子中,UserService类依赖于Database接口,而不是具体的数据库实现。

这意味着如果我们想更换数据库,只需要传入不同的实现类,而不需要修改UserService类本身。这样做遵循了依赖倒置原则,降低了模块之间的耦合度。

从这个例子可知,定义数据库抽象层供不同的数据库实现,然后在UserService类依赖于抽象接口而不是具体实现,可以充分降低代码耦合度,可以快速拓展其他数据库实现。

06.用户购买家电案例

例子1:违反依赖倒置原则。假设顾客,想要买冰箱,洗衣机,电视,或者其他电器,它直接依赖于Customer类来进行数据操作。

public class Customer {
   
    public void buyFridge() {
   
        System.out.println("购买冰箱");
    }

    public void buyTelevision() {
   
        System.out.println("购买电视");
    }
}

例子2:遵循依赖倒置原则。可以通过引入一个抽象层来遵循依赖倒置原则。

/**
 * 商品接口
 */
public interface IGood {
   
    /**
     * 购买商品
     */
    void buy();
}

public class Customer {
   
    public void buy(IGood iGood) {
   
        iGood.buy();
    }
}

/**
 * 冰箱商品
 */
public class FridgeGood implements IGood {
   
    @Override
    public void buy() {
   
        System.out.println("购买冰箱");
    }
}

/**
 * 电视商品
 */
public class TelevisionGood implements IGood {
   
    @Override
    public void buy() {
   
        System.out.println("购买电视");
    }
}

/**
 * 洗衣机商品
 */
public class WashMachineGood implements IGood {
   
    @Override
    public void buy() {
   
        System.out.println("购买洗衣机");
    }
}

public class Main {
   
    public static void main(String[] args) {
   
        Customer customer = new Customer();
        customer.buy(new FridgeGood());
        customer.buy(new TelevisionGood());
        customer.buy(new WashMachineGood());
    }
}

07.发送消息的案例

例子1:违反依赖倒置原则。假设用户,可以通过邮件,短信息,信件等发送消息,下面这种就不太友好。

public class Notification {
   
    public void email(String cellphone, String message) {
   
        System.out.println("通过邮件发送消息");
    }

    public void message(String cellphone, String message) {
   
        System.out.println("通过短信息发送消息");
    }

    public void letter(String cellphone, String message) {
   
        System.out.println("通过信件发送消息");
    }
}

例子2:遵循依赖倒置原则。可以通过引入一个抽象层来遵循依赖倒置原则。

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);
  }
}

public interface MessageSender {
   
  void send(String cellphone, String message);
}

// 邮件发送类
public class EmailSender implements MessageSender {
   
  @Override
  public 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) {
   
    //....
  }
}

08.依赖倒置VS依赖注入

8.1 理解依赖注入

‌依赖注入(Dependency Injection, DI)是一种设计模式,旨在将对象的依赖关系从对象内部转移到外部管理,从而降低类之间的耦合度,提高代码的可维护性和可测试性‌。‌

8.2 依赖注入方式

依赖注入(Dependency Injection,DI)是一种通过将依赖关系从高层模块解耦的方式,常用于实现依赖倒置原则。

  1. 构造函数注入:通过在类的构造函数中声明依赖参数,将依赖关系通过构造函数传递给类的实例。最常见的依赖注入方式。
  2. Setter方法注入:通过提供一组setter方法,允许外部代码设置依赖对象。这种方式相对于构造函数注入更灵活,可以在对象创建后随时更改依赖。
  3. 接口注入:通过在类中定义一个接口,该接口包含用于注入依赖的方法。类实现该接口,并通过接口方法接收依赖对象。这种方式相对较少使用,因为它引入了更多的接口和方法。
  4. 属性注入:通过在类中声明依赖对象的属性,并提供相应的setter方法,将依赖对象注入到属性中。可能导致类的实例在没有依赖对象的情况下被创建。

8.3 两者有何区别

依赖倒置原则(Dependency Inversion Principle,DIP)和依赖注入(Dependency Injection,DI)是面向对象设计中两个相关但不同的概念。

  1. 依赖倒置原则是一种设计原则,它指导我们在设计软件时应该依赖于抽象而不是具体实现。高层模块不应该直接依赖于低层模块,而是通过抽象接口或基类来进行依赖。
  2. 依赖注入是一种实现依赖倒置原则的具体技术,它通过将依赖关系从高层模块解耦,将依赖对象注入到类的实例中。依赖注入有多种方式,如构造函数注入、setter方法注入、属性注入等。它的目的是通过外部注入依赖对象,而不是在类内部创建或获取依赖对象。

依赖注入是实现依赖倒置原则的一种常见方式,但并不是唯一的方式。依赖倒置原则还可以通过工厂模式、策略模式等其他设计模式来实现。

09.依赖倒置的思考

9.1 依赖倒置优点

  1. 降低耦合性:高层模块和低层模块之间通过抽象进行解耦,避免了对具体实现的依赖。
  2. 增强可扩展性:通过依赖抽象,可以更容易地替换或扩展具体实现,而不需要修改高层模块。
  3. 提高可维护性:模块之间的解耦使得系统的维护变得更加容易,修改一个模块的实现不会影响到其他模块。

9.2 依赖倒置缺点

  1. 增加系统的复杂性:为了遵循依赖倒置原则,通常需要引入额外的抽象层次,这可能会增加系统的复杂性。
  2. 可能导致过度设计:在简单系统中,如果过度使用依赖倒置原则,可能导致不必要的复杂性,造成过度设计。

9.3 在设计模式的体现

  1. 工厂模式(Factory Pattern):通过定义抽象工厂接口,让具体的工厂类实现该接口,从而将对象的创建过程与使用过程解耦。这样,高层模块只依赖于抽象工厂接口,而不依赖于具体的产品类。这符合依赖倒置原则,使得高层模块依赖于抽象而非具体实现。
  2. 观察者模式(Observer Pattern):观察者依赖于被观察者的抽象接口,而不依赖于具体的被观察者。这样,当被观察者发生变化时,观察者可以接收到通知并做出相应的处理,而不需要直接依赖于具体的被观察者。
  3. 策略模式(Strategy Pattern):通过定义一个抽象策略接口,让具体的策略类实现该接口,从而将算法的选择与使用解耦。高层模块通过依赖于抽象策略接口,而不依赖于具体的策略类,实现了依赖倒置原则。

这些设计模式的共同点是,它们都通过引入抽象接口或基类,将高层模块与具体实现解耦,使得高层模块依赖于抽象而非具体实现。符合依赖倒置原则的设计思想。

10.依赖倒置原则总结

  1. 依赖倒置问题思考:“依赖倒置”这个概念指的是“谁跟谁”的“什么依赖”被反转了?“倒置”两个字该如何理解?
  2. 学习该原则目标:搞懂该原则作用,掌握实现方式,并且应用到实际案例项目中。
  3. 依赖倒置的定义:高层模块不应该依赖底层模块,应该依赖于抽象。换句话说,设计中应当依赖于接口或抽象类,而不是依赖于具体实现。
  4. 依赖倒置原则思想:通过依赖抽象层次(如接口、抽象类),可以让高层模块不依赖具体的实现细节,从而使得系统更具扩展性和灵活性。
  5. 多数据库操作案例:从这个例子可知,定义数据库抽象层供不同的数据库实现,然后在UserService类依赖于抽象接口而不是具体实现,可以充分降低代码耦合度,可以快速拓展其他数据库实现。
  6. 依赖注入如何理解:依赖注入(Dependency Injection,DI)是一种通过将依赖关系从高层模块解耦的方式,常用于实现依赖倒置原则。
  7. 依赖注入有哪些方式:1.构造函数注入;2.Setter方法注入;3.接口注入;4.属性注入
  8. 依赖倒置和依赖注入区别:依赖倒置原则是一种设计原则,依赖注入是一种实现依赖倒置原则的具体技术。
  9. 依赖倒置有哪些缺点:为了遵循依赖倒置原则,通常需要引入额外的抽象层次,这可能会增加系统的复杂性。
  10. 在设计模式的体现有哪些:工厂模式,观察者模式,策略模式,这些设计模式的共同点是,它们都通过引入抽象接口或基类,将高层模块与具体实现解耦,使得高层模块依赖于抽象而非具体实现。符合依赖倒置原则的设计思想。

11.更多内容推荐

模块 描述 备注
GitHub 多个YC系列开源项目,包含Android组件库,以及多个案例 GitHub
博客汇总 汇聚Java,Android,C/C++,网络协议,算法,编程总结等 YCBlogs
设计模式 六大设计原则,23种设计模式,设计模式案例,面向对象思想 设计模式
Java进阶 数据设计和原理,面向对象核心思想,IO,异常,线程和并发,JVM Java高级
网络协议 网络实际案例,网络原理和分层,Https,网络请求,故障排查 网络协议
计算机原理 计算机组成结构,框架,存储器,CPU设计,内存设计,指令编程原理,异常处理机制,IO操作和原理 计算机基础
学习C编程 C语言入门级别系统全面的学习教程,学习三到四个综合案例 C编程
C++编程 C++语言入门级别系统全面的教学教程,并发编程,核心原理 C++编程
算法实践 专栏,数组,链表,栈,队列,树,哈希,递归,查找,排序等 Leetcode
Android 基础入门,开源库解读,性能优化,Framework,方案设计 Android
目录
相关文章
什么是依赖倒置原则
依赖倒置原则(Dependency Inversion Principle, DIP)是面向对象设计中的SOLID原则之一,强调高层模块不应依赖低层模块,双方应依赖于抽象。该原则包含两方面:抽象不依赖细节,细节依赖抽象。这有助于降低耦合度、提高模块化和灵活性。实践中可通过接口定义契约、依赖注入等方式实现。例如,在Java中定义`MessageService`接口及其实现`EmailService`,高层`NotificationService`依赖于`MessageService`接口而非具体实现,从而实现了对扩展开放、对修改关闭的设计目标。
386 1
|
5月前
|
设计模式 网络协议 Java
09.接口vs抽象类比较
本文详细对比了接口与抽象类的区别及应用场景,涵盖两者的基本概念、特性以及设计思想。通过具体案例分析,如日志记录和过滤器功能,阐明抽象类适用于代码复用(is-a关系),而接口侧重解耦和行为定义(has-a关系)。此外,还探讨了如何在不支持接口或抽象类的语言中模拟其实现,并总结了选择两者的判断标准。文章结合实际开发场景,提供了清晰的指导,帮助开发者更好地理解与应用这两种核心面向对象概念。
258 26
|
6月前
|
设计模式 Java API
05.接口隔离原则介绍
接口隔离原则(ISP)是SOLID原则之一,强调客户端不应依赖于它们不需要的接口。通过将庞大而臃肿的接口拆分为更小、更具体的接口,确保每个接口只包含客户端真正需要的方法,从而提高代码的可维护性和灵活性。本文详细介绍了接口隔离原则的概念、核心思想、实现方式及案例分析,并对比了其与单一职责原则的区别。关键点包括:接口应精简、独立且可扩展,避免强迫实现不必要的方法,减少系统的耦合性。
278 19
|
6月前
|
设计模式 网络协议 Java
04.里式替换原则介绍
里式替换原则(LSP)是面向对象设计的重要原则之一,确保子类可以无缝替换父类而不破坏程序功能。本文详细介绍了LSP的定义、背景、理解方法及应用场景,通过电商支付和鸟类飞行案例展示了如何遵循LSP,并分析了其优缺点。LSP强调子类应保持父类的行为一致性,有助于提高代码的可扩展性、可维护性和可重用性,但也可能导致过度设计。最后,对比了LSP与多态的区别,明确了LSP作为设计原则的重要性。
218 4
|
6月前
|
Cloud Native 算法 中间件
如何使用服务网格实现全方位的流量调度场景
阿里云服务网格(ASM)通过流量调度套件扩展了Istio的限流、熔断能力,实现分用户限流、请求排队等复杂流量管理功能,提升分布式系统高可用性与可观测性。
|
7月前
|
设计模式 网络协议 Java
03.开闭原则详细介绍
本文详细介绍了面向对象设计中的开闭原则(OCP),即软件实体应对扩展开放、对修改关闭。文章通过问题思考、背景分析、实现方式及案例教学,深入探讨了如何在实际开发中应用这一原则,以提高代码的可维护性和扩展性。文中还结合图形绘制和银行业务两个具体案例,展示了如何通过抽象类、接口、策略模式等技术手段实现开闭原则。最后总结了开闭原则的优缺点,并强调了其在设计模式中的重要地位。
142 14
|
9月前
|
设计模式 网络协议 Java
02.单一职责原则详解
单一职责原则(SRP)是面向对象设计的重要原则,强调一个类或模块应仅负责完成一个特定的职责或功能。通过将复杂的功能分解为多个粒度小、功能单一的类,可以提高系统的灵活性、可维护性和可扩展性。本文详细介绍了如何理解单一职责原则,包括方法、接口和类层面的应用,并通过具体例子解释了其优势和判断标准。此外,还探讨了在实际开发中如何平衡类的设计,避免过度拆分导致的复杂性增加。
293 5
|
12月前
|
存储 负载均衡 NoSQL
一文让你搞懂 zookeeper
一文让你搞懂 zookeeper
14900 15
|
消息中间件 Java 数据库
Spring Boot中如何实现分布式事务
Spring Boot中如何实现分布式事务
|
存储 程序员 数据处理
【软件设计师】程序猿需掌握的技能——数据流图
【软件设计师】程序猿需掌握的技能——数据流图

热门文章

最新文章