依赖注入(DI)与控制反转(IOC)

控制反转(IOC)

控制反转(IoC),也称为反转控制(Inversion of Control),是一种软件设计原则,它反转了传统的程序控制流程。

在传统的程序中,应用程序代码通常负责创建和管理对象、依赖项以及它们之间的关系。

而在 IoC 中,控制流程的控制权从应用程序代码中转移到了外部的容器或框架中。

IoC 的主要思想是:

  1. 将控制权移交给容器或框架:在 IoC 中,应用程序的主控制流不再由应用程序代码直接控制。相反,这些控制权被移交给了外部的容器或框架,它们负责管理对象的创建、生命周期、依赖解析等。

  2. 减少耦合性:IoC 有助于减少组件和类之间的紧耦合性。组件不再直接依赖于具体的实现,而是依赖于接口或抽象,容器则负责提供具体的实现。

  3. 增加可扩展性:由于应用程序代码不再负责对象的创建和管理,因此很容易扩展和替换组件,而无需修改大量代码。

IoC 可以通过以下方式实现:

  • 依赖注入(DI):依赖注入是 IoC 的一种实现方式,它通过将依赖项注入到类的构造函数、方法或属性中,将对象的创建和依赖解析交给了外部容器。这有助于降低组件之间的耦合度。

  • 服务定位器:服务定位器是一种中央注册表或容器,用于存储和检索对象实例。组件可以通过服务定位器获取所需的依赖项,而不需要直接依赖于具体的实现。

  • 事件驱动编程:IoC 也可以通过事件驱动编程来实现,其中组件通过发布和订阅事件来通信,而不是直接调用其他组件的方法。

IoC 是许多现代框架和库的核心原则,包括 Spring(Java)、Angular(TypeScript)、ASP.NET Core(C#)等。.

它有助于提高代码的可维护性、可测试性和可扩展性,使应用程序更容易适应变化和增长。

1.1 理解控制反转例子 1

理解控制反转(IoC)的概念可以通过一个更深入的例子来加强。以下是一个说明 IoC 如何改变应用程序的结构和控制流程的例子。

考虑一个简单的电商应用程序,它需要加载产品信息并显示给用户。在没有 IoC 的情况下,应用程序可能会像这样设计:

class ProductService {
  // 产品服务负责加载产品信息
  getProducts() {
    // 从服务器加载产品信息的代码
  }
}

class ProductComponent {
  constructor() {
    this.productService = new ProductService(); // 直接创建 ProductService 实例
  }

  displayProducts() {
    const products = this.productService.getProducts(); // 直接调用 ProductService 方法
    // 显示产品信息给用户的代码
  }
}

在这个例子中,ProductComponent 直接创建了一个 ProductService 实例,并调用了它的方法来加载产品信息。这种设计有一些问题:

  1. 紧耦合性ProductComponentProductService 紧密耦合在一起,无法轻松替换或扩展服务。

  2. 难以测试:由于 ProductComponent 直接创建了 ProductService 实例,测试 ProductComponent 时难以模拟或替代 ProductService

  3. 可扩展性差:如果以后需要替换 ProductService 或添加其他服务,就需要修改 ProductComponent 的代码。

现在,让我们使用 IoC 原则来改进这个例子。我们将控制权交给外部容器,容器负责创建并注入依赖项。

class ProductService {
  getProducts() {
    // 从服务器加载产品信息的代码
  }
}

class ProductComponent {
  constructor(productService) {
    this.productService = productService; // 容器注入 ProductService 实例
  }

  displayProducts() {
    const products = this.productService.getProducts(); // 使用注入的 ProductService 实例
    // 显示产品信息给用户的代码
  }
}

// 容器(IoC容器)
const productService = new ProductService();
const productComponent = new ProductComponent(productService); // 容器注入 ProductService

在这个改进后的例子中:

  • ProductService 不再由 ProductComponent 直接创建,而是由外部容器创建并注入。
  • ProductComponent 不需要知道 ProductService 是如何创建的,它只关心如何使用它。
  • 这种设计降低了紧耦合性,使得容易替换或扩展服务。
  • 这也使得在测试时更容易模拟或替代 ProductService

这个例子演示了 IoC 如何反转控制流程,将对象的创建和依赖解析交给外部容器,从而提高了代码的可维护性、可测试性和可扩展性。通过这种方式,应用程序变得更加灵活,可以更容易地适应变化和增长。

1.2 理解控制反转例子2

当涉及 IoC 时,另一个常见的示例涉及数据库连接。在没有 IoC 的情况下,一个典型的应用程序可能会直接创建和管理数据库连接,但通过 IoC,我们可以改变这种情况。

考虑以下没有使用 IoC 的数据库连接示例:

public class DatabaseService {
    private Connection connection;

    public DatabaseService() {
        // 直接在构造函数中创建数据库连接
        connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "username", "password");
    }

    public ResultSet executeQuery(String sql) throws SQLException {
        Statement statement = connection.createStatement();
        return statement.executeQuery(sql);
    }
}

public class UserService {
    private DatabaseService databaseService;

    public UserService() {
        databaseService = new DatabaseService(); // 直接创建 DatabaseService 实例
    }

    public User getUser(int userId) throws SQLException {
        ResultSet resultSet = databaseService.executeQuery("SELECT * FROM users WHERE id = " + userId);
        // 解析结果集并返回用户对象
    }
}

在这个例子中,DatabaseServiceUserService 直接负责创建和管理数据库连接。这样的设计存在以下问题:

  1. 紧耦合性UserServiceDatabaseService 紧密耦合在一起,难以替换或扩展数据库访问方式。

  2. 难以测试:测试 UserService 时难以模拟或替代 DatabaseService

  3. 可扩展性差:如果以后需要更改数据库访问方式或连接参数,就需要修改 UserService 的代码。

现在,让我们使用 IoC 原则改进这个例子。我们将控制权交给外部容器,容器负责创建并注入数据库服务。

public class DatabaseService {
    private Connection connection;

    public DatabaseService(String url, String username, String password) throws SQLException {
        // 在构造函数中接受连接参数并创建数据库连接
        connection = DriverManager.getConnection(url, username, password);
    }

    public ResultSet executeQuery(String sql) throws SQLException {
        Statement statement = connection.createStatement();
        return statement.executeQuery(sql);
    }
}

public class UserService {
    private DatabaseService databaseService;

    public UserService(DatabaseService databaseService) {
        this.databaseService = databaseService; // 容器注入 DatabaseService 实例
    }

    public User getUser(int userId) throws SQLException {
        ResultSet resultSet = databaseService.executeQuery("SELECT * FROM users WHERE id = " + userId);
        // 解析结果集并返回用户对象
    }
}

// 容器(IoC容器)
public class ApplicationContainer {
    public static void main(String[] args) throws SQLException {
        DatabaseService databaseService = new DatabaseService("jdbc:mysql://localhost:3306/mydb", "username", "password");
        UserService userService = new UserService(databaseService); // 容器注入 DatabaseService
        // 使用 userService
    }
}

在这个改进后的例子中:

  • DatabaseService 不再由 UserService 直接创建,而是由外部容器创建并注入。
  • UserService 不需要知道 DatabaseService 是如何创建的,它只关心如何使用它。
  • 这种设计降低了紧耦合性,使得容易替换或扩展数据库服务。
  • 这也使得在测试时更容易模拟或替代 DatabaseService

这个例子演示了 IoC 如何改变应用程序中的对象创建和依赖解析方式,以提高代码的可维护性、可测试性和可扩展性。

依赖注入(DI)

依赖注入是实现控制反转的一种具体方式,它有助于降低组件和服务之间的耦合度,使代码更容易测试、维护和扩展。

控制反转强调将控制权交给框架或容器,以提高代码的可扩展性和灵活性。

在 Angular 8 中,依赖注入是实现 IoC 的主要机制之一,用于管理应用程序中各种组件之间的依赖关系。

以下是在 Angular 8 中进行依赖注入的基本步骤:

  1. 创建服务(Service):首先,您需要创建一个可注入的服务。您可以使用 Angular CLI 来生成一个新的服务,或者手动创建一个类并用@Injectable()装饰器标记它,以使其成为可注入的服务。

    例如:

    import { Injectable } from "@angular/core";
    
    @Injectable({
      providedIn: "root", // 声明服务在根注入器中可用
    })
    export class MyService {
      // 服务的方法和属性
    }
    
  2. 提供服务(Provide Service):在应用程序的根模块或组件中,使用providers数组或@Injectable()装饰器的providedIn属性来提供服务。这会告诉 Angular 如何创建和管理服务的实例。

    • 使用providers数组:

      import { NgModule } from "@angular/core";
      import { MyService } from "./my-service";
      
      @NgModule({
        providers: [MyService], // 声明服务的提供者
        // ...
      })
      export class AppModule {}
      
    • 使用providedIn属性:

      import { Injectable } from "@angular/core";
      
      @Injectable({
        providedIn: "root", // 声明服务在根注入器中可用
      })
      export class MyService {
        // 服务的方法和属性
      }
      
  3. 注入服务(Inject Service):在组件、服务或其他需要使用服务的地方,通过构造函数注入服务。

    import { Component } from "@angular/core";
    import { MyService } from "./my-service";
    
    @Component({
      selector: "app-my-component",
      template: "<p>{{ myService.someMethod() }}</p>",
    })
    export class MyComponent {
      constructor(private myService: MyService) {
        // 可以在这里使用myService
      }
    }
    

Angular 会自动处理服务的创建和管理,确保服务的单一实例在整个应用程序中共享。这有助于组织和维护 Angular 应用程序的代码。

Contributors: masecho