12 设计原则SOLID之 :D 依赖反转原则

882 阅读6分钟

写代码太菜了,被女朋友我分手第一天,来学习 依赖反转原则,如果你还有女朋友,那你呀好好珍惜,如果你没有,哈哈哈哈哈,那一定是代码写的太烂了!

1.什么是依赖反转原则?

1. 定义

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

上面这句话有点抽象,首先通过例子,先有点印象,再去进一步解读什么是依赖反转。 假设有一个报告生成的场景,我们有两个类:Report 用于生成报告,和 Printer 用于将报告打印出来。一开始的设计可能如下:

class Report {
    public String generate() {
        return "This is a report.";
    }
}

class ReportClient {
    private Report report;

    public ReportClient() {
        this.report = new Report();  //组合
    }

    public void generateAndPrintReport() {
        String content = report.generate();
        System.out.println(content);
    }
}

在这个设计中,ReportClient 高层模块直接依赖于 Report 低层模块。如果需要修改或添加不同类型的报告,就需要修改 ReportClient,违反了开闭原则。

修改之后:

// 面向接口编程
interface IReport {
    String generate();
}

// 具体实现
class Report implements IReport {
    public String generate() {
        return "This is a report.";
    }
}

// 依赖反转原则
class ReportClient {
    private IReport report;

    public ReportClient(IReport report) {
        this.report = report;
    }

    public void generateAndPrintReport() {
        String content = report.generate();
        System.out.println(content);
    }
}

现在,ReportClient 依赖于抽象接口 IReport,而不是具体的 Report 类。这样,我们可以轻松地引入新的报告类型,而不影响 ReportClient 的代码。

现在再来理解依赖反转原则要求

  1. 高层模块: 这是应用的主要逻辑,它应该依赖于抽象,而不是具体的实现细节(接口,抽象类)。
  2. 抽象: 这是一个接口或者抽象类,它定义了高层模块所需的行为(不变的,固定的)。
  3. 低层模块: 这是实际执行行为的类,它应该依赖于相同的抽象(多变的,可拔插)。

其核心思想是:要面向接口编程(OOD),不要面向实现编程。看似在讲依赖反转,实际在讲面向接口编程,哈哈哈哈哈。

上面的例子用到了抽象也用到了多态,前面学习的抽象和多态的关系都忘了,在这里复习下:

  • 焦点不同: 抽象关注的是对象的本质特征和行为的抽取,而多态关注的是同一操作在不同对象上的不同行为。
  • 实现方式不同: 抽象通过抽象类和接口来定义规范,而多态通继承和接口实现,使得同一操作可以作用在不同的对象上,产生不同的结果。
  • 应用层次不同: 抽象是在设计层次上进行的,用于建模和设计系统的结构;而多态则是在实现和运行阶段体现的,用于提高代码的灵活性和可维护性。

如果只看依赖反转,拿到这里就行了,如果想了解控制反转、依赖反转、依赖注入,那可以看下下面。

2.拓展:控制反转、依赖反转、依赖注入的关系

1. 控制反转(Inversion of Control,IoC):

  • 概念: IoC是一种软件设计思想,它反转了传统的程序设计中控制流的方向。在传统的程序设计中,程序员编写主体部分并控制其中的逻辑流程,而在IoC中,控制流程由框架或容器负责,程序员只需提供相应的扩展点或插件。
  • 实现: IoC可以通过使用设计模式(例如模板模式、策略模式)、依赖注入等方式来实现。Spring框架是一个典型的IoC容器,它通过反射和配置文件等机制实现了IoC。

以下是一个简单的示例,假设有一个简单的Java类 UserService,它负责用户的业务逻辑:

public class UserService {
    private UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void createUser(String username, String email) {
        // 业务逻辑,例如创建用户并保存到数据库
        User user = new User(username, email);
        userRepository.save(user);
    }
}

在传统的方式中,你需要手动创建 UserService 类的实例,并注入它所依赖的 UserRepository

public class MyApp {
    public static void main(String[] args) {
        UserRepository userRepository = new UserRepositoryImpl();
        UserService userService = new UserService(userRepository);
        
        // 使用 userService 进行业务操作
        userService.createUser("john_doe", "john@example.com");
    }
}

而在IoC框架中,例如Spring框架,你只需配置好依赖关系,框架负责创建和管理对象,如下所示:

public class MyApp {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService userService = context.getBean(UserService.class);
        
        // 使用 userService 进行业务操作
        userService.createUser("john_doe", "john@example.com");
    }
}

2. 依赖反转原则(Dependency Inversion Principle,DIP):

  • 概念: DIP是SOLID原则中的一项,它强调高层模块不应该依赖于低层模块,两者都应该依赖于抽象。这是为了减小系统中模块之间的耦合度。
  • 实现: DIP可以通过使用接口、抽象类等方式实现,目的是将高层模块和低层模块的依赖关系反转,使得高层模块依赖于抽象,而不是具体实现。依赖注入是一种常见的实现方式。

3. 依赖注入(Dependency Injection,DI):

  • 概念: DI是一种实现依赖反转的具体技术,它通过将依赖关系从高层模块注入到低层模块中,以达到减小模块之间耦合度的目的。通常有三种形式的依赖注入:构造函数注入、属性注入和方法注入。
  • 实现: DI可以手动实现,也可以通过使用框架(如Spring)来自动进行依赖注入。框架负责在程序运行时将依赖关系注入到相应的类中。
  • 不通过 new() 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。

举个例子: 假设有一个 UserService 类,它依赖于一个 UserRepository 接口来执行用户相关的数据库操作:

public class UserService {
    private UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void createUser(String username, String email) {
        // 业务逻辑,例如创建用户并保存到数据库
        User user = new User(username, email);
        userRepository.save(user);
    }
}

在这个例子中,UserService 通过构造函数接收一个 UserRepository 的实例。这就是依赖注入的一种形式。

1. 构造函数注入:

public class MyApp {
    public static void main(String[] args) {
        UserRepository userRepository = new UserRepositoryImpl(); // 具体实现
        UserService userService = new UserService(userRepository);
        
        // 使用 userService 进行业务操作
        userService.createUser("john_doe", "john@example.com");
    }
}

在这个例子中,通过构造函数将 UserRepository 注入到 UserService 中。

2. Setter 方法注入:

public class MyApp {
    public static void main(String[] args) {
        UserRepository userRepository = new UserRepositoryImpl(); // 具体实现
        UserService userService = new UserService();
        userService.setUserRepository(userRepository); // 使用 setter 方法进行注入
        
        // 使用 userService 进行业务操作
        userService.createUser("john_doe", "john@example.com");
    }
}

3. 接口注入:

public class MyApp {
    public static void main(String[] args) {
        UserRepository userRepository = new UserRepositoryImpl(); // 具体实现
        UserService userService = new UserService();
        userService.injectUserRepository(userRepository); // 使用接口注入
        
        // 使用 userService 进行业务操作
        userService.createUser("john_doe", "john@example.com");
    }
}

这些例子展示了依赖注入的不同形式,它们都达到了将依赖关系从 UserService 中解耦的目的。依赖注入使得代码更加灵活、可测试和可维护,因为依赖关系不再硬编码在类内部,而是由外部提供。

关系:

  • IoC是一种思想或概念,它强调控制流的反转,让框架或容器负责程序的控制流程。
  • DIP是一项原则,它强调高层模块和低层模块之间的依赖关系应该通过抽象来进行,减小耦合。
  • DI是一种实现IoC和DIP的具体技术,它通过将依赖关系注入到类中,实现了控制反转和依赖反转。