当前位置: 代码迷 >> 综合 >> 编程思想: 控制反转(Inversion of Control - IoC)
  详细解决方案

编程思想: 控制反转(Inversion of Control - IoC)

热度:80   发布时间:2023-12-15 10:59:03.0

本文参考PHP开发框架phalcon的文档1. 它从一个简单的例子出发, 描述了编码中遇到的一系列问题, 然后一步步去解决, 最后得到一个解决方案. 在这个例子中我们了解到:

  • 一种设计模式: 依赖注入(Dependency Injection)
  • 控制反转是什么?
  • 控制反转是为了解决什么问题?

在这个例子中, 我们要写一个类SomeComponent来实现某个功能. 由于它依赖连接数据库, 我们把对数据库的连接以及相关操作写在方法doDbTask中.

  1. 配置写死在代码中
// SomeComponent.java
public class SomeComponent {public void doDbTask() throws Exception {// 数据库连接的配置写死在代码中Class.forName("com.mysql.jdbc.Driver");Connection connection = DriverManager.getConnection("url","user","password");// ...}
}

代码写死导致我们不能更改连接的配置, 显然无法满足实际需求.

  1. 依赖注入.

为了解决上述问题, 我们可以把connection对象注入到SomeComponent的实例. 一种常用的方式是把依赖的对象当作SomeComponent的构造函数的参数, 称为构造器注入. (其它注入方式可以参考wiki2)

// SomeComponent.java
public class SomeComponent {private Connection connection;public SomeComponent(Connection connection) {this.connection = connection;}public void doDbTask() throws Exception {Connection connection = connection;// ...}
}// Client.java
public class Client {public void useSomeComponent throws Exception {Class.forName("com.mysql.jdbc.Driver");Connection connection = DriverManager.getConnection("url","user","password");SomeComponent someComponent = new SomeComponent(connection);someComponent.doDbTask();}
}

现在假设很多模块都要使用SomeComponent, 因此每个模块都需要初始化一个Connection的实例. 这样不仅麻烦, 而且不能复用数据库连接, 造成资源浪费.

  1. 把依赖的对象放入容器.
// Container.java
public class Container {private static Connection connection;/*** 创建数据库连接.*/private static void createConnection() throws Exception {Class.forName("com.mysql.jdbc.Driver");connection = DriverManager.getConnection("url","user","password");}/*** 获取已有的数据库连接,* 不存在则创建新的连接.*/public static Connection getConnection() throws Exception {if(connection == null) createConnection();return connection;}
}// Client.java
public class Client {public void useSomeComponent() throws Exception {// 从容器中获取Connection的实例SomeComponent someComponent = new SomeComponent(Container.getConnection());someComponent.doDbTask();}
}

现在假设SomeComponent依赖很多模块, 除了Connection之外, 它还依赖FileSystem, HttpClient, HttpCookie. 按照上面的方法(工厂模式3), 首先要把依赖的对象作为SomeComponent的构造函器参数.

// SomeComponent.java
public class SomeComponent {private Connection connection;private FileSystem fileSystem;private HttpClient httpClient;private HttpCookie httpCookie;public SomeComponent(Connection connection, FileSystem fileSystem, HttpClient httpClient, HttpCookie httpCookie) {this.connection = connection;this.fileSystem = fileSystem;this.httpClient = httpClient;this.httpCookie = httpCookie;}public void doDbTask() throws Exception {Connection conn = connection;// ...}
}

其次, 在Container中实例化新的依赖对象fileSystem, httpClient, httpCookie.

// Container.java
public class Container {private static Connection connection;private static FileSystem fileSystem;private static HttpClient httpClient;private static HttpCookie httpCookie;/*** 创建数据库连接.*/private static void createConnection() throws Exception {Class.forName("com.mysql.jdbc.Driver");connection = DriverManager.getConnection("url","user","password");}/*** 获取已有的数据库连接,* 不存在则创建新的连接.*/public static Connection getConnection() throws Exception {if(connection == null) createConnection();return connection;}/*** 实例化FileSystem对象.*/public static void createFileSystem() { // ... }/*** 获取FileSystem实例, * 不存在则创建新的实例.*/public static FileSystem getFileSystem() {if(fileSystem == null) createFileSystem();return fileSystem;}/*** 实例化HttpClient对象.*/public static void createHttpClient() { // ... }/*** 获取HttpClient实例, * 不存在则创建新的实例.*/public static HttpClient getHttpClient() {if(httpClient == null) createHttpClient();return httpClient;}/*** 实例化HttpCookie对象.*/public static void createHttpCookie() { // ... }/*** 获取HttpCookie实例, * 不存在则创建新的实例.*/public static HttpCookie getHttpCookie() {if(httpCookie == null) createHttpCookie();return httpCookie;}
}

Client可以通过Container获取Connection, FileSystem, HttpClient, HttpCookie的实例, 从而初始化SomeCoponent.

// Client.java
public class Client {public void useSomeComponent() throws Exception {// 从容器中获取Connection的实例SomeComponent someComponent = new SomeComponent(Container.getConnection(), Container.getFileSystem(), Container.getHttpClient(), Container.getHttpCookie());someComponent.doDbTask();}
}

等等, 似乎有些问题. Client实际上依赖两个组件: SomeComponentContainer. 当SomeComponent的依赖发生变化时:

  1. 开发者需要修改SomeComponent的依赖, 并把依赖的类在Container中实例化.
  2. 由于SomeComponent的构造函数发生了变化, Client中用来实例化SomeComponent对象的代码需要做相应的修改.

这样一来, SomeComponent的修改会导致ContainerClient的修改. 换句话说, 实际上又回到了当初写死代码的情形.

  1. 控制反转

为了克服上面的问题, 一个解决思路是把Container的维护工作交给框架(例如Java的Spring, Php的Phalcon, JS的AngularX)来完成, 即通过一些配置使得框架能 发现 SomeComponent的依赖对象. 当SomeComponent需要使用这些对象的时候由框架来完成实例化的工作. 这样一来, 当SomeComponent的依赖发生变化时, 开发者只需要修改SomeComponent和相关依赖的配置, 而所有依赖SomeComponent的应用程序不需要做修改. 这种思路被称为 控制反转, 即依赖对象的 控制权 (即对象的生成和销毁)从开发者手上转移到框架.

以Springboot为例, 按框架的形式写好SomeComponent之后, 如果我们需要使用SomeComponent, 大致写法如下(详细教程可参考网上的公开教程或使用IntelliJ IDEA构建Spring Boot项目示例):

// Client.java
public class Client{@Autowired  // 由框架自动生成对象private SomeComponent someComponent;public Client(SomeComponent someComponent) {this.someComponent = someComponent;}public void useSomeComponent() throws Exception {someComponent.doDbTask();}
}

Remark

  1. 控制反转试图解决的是在 同一个开发框架下, 模块之间的解耦和复用的问题.
  2. 框架的出现或多或少是为了解决开发语言在某些方面的缺陷. 有些编程语言(例如Python)就能自然做到解耦和复用, 而无需依赖额外的框架(想想为什么).

  1. https://docs.phalcon.io/3.4/en/di ??

  2. https://en.wikipedia.org/wiki/Dependency_injection ??

  3. https://en.wikipedia.org/wiki/Factory_method_pattern ??

  相关解决方案