依赖注入(DI)与控制反转(IOC)
控制反转(IOC)
控制反转(IoC),也称为反转控制(Inversion of Control),是一种软件设计原则,它反转了传统的程序控制流程。
在传统的程序中,应用程序代码通常负责创建和管理对象、依赖项以及它们之间的关系。
而在 IoC 中,控制流程的控制权从应用程序代码中转移到了外部的容器或框架中。
IoC 的主要思想是:
将控制权移交给容器或框架:在 IoC 中,应用程序的主控制流不再由应用程序代码直接控制。相反,这些控制权被移交给了外部的容器或框架,它们负责管理对象的创建、生命周期、依赖解析等。
减少耦合性:IoC 有助于减少组件和类之间的紧耦合性。组件不再直接依赖于具体的实现,而是依赖于接口或抽象,容器则负责提供具体的实现。
增加可扩展性:由于应用程序代码不再负责对象的创建和管理,因此很容易扩展和替换组件,而无需修改大量代码。
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
实例,并调用了它的方法来加载产品信息。这种设计有一些问题:
紧耦合性:
ProductComponent
与ProductService
紧密耦合在一起,无法轻松替换或扩展服务。难以测试:由于
ProductComponent
直接创建了ProductService
实例,测试ProductComponent
时难以模拟或替代ProductService
。可扩展性差:如果以后需要替换
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);
// 解析结果集并返回用户对象
}
}
在这个例子中,DatabaseService
和 UserService
直接负责创建和管理数据库连接。这样的设计存在以下问题:
紧耦合性:
UserService
与DatabaseService
紧密耦合在一起,难以替换或扩展数据库访问方式。难以测试:测试
UserService
时难以模拟或替代DatabaseService
。可扩展性差:如果以后需要更改数据库访问方式或连接参数,就需要修改
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 中进行依赖注入的基本步骤:
创建服务(Service):首先,您需要创建一个可注入的服务。您可以使用 Angular CLI 来生成一个新的服务,或者手动创建一个类并用
@Injectable()
装饰器标记它,以使其成为可注入的服务。例如:
import { Injectable } from "@angular/core"; @Injectable({ providedIn: "root", // 声明服务在根注入器中可用 }) export class MyService { // 服务的方法和属性 }
提供服务(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 { // 服务的方法和属性 }
注入服务(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 应用程序的代码。