Java 编程之控制反转

目录

本文参考 PHP 开发框架 phalcon的文档1。它从一个简单的例子出发,描述了编码中遇到的一系列问题,然后一步步去解决,最后得到一个解决方案,也就是控制反转(Inversion of Control – IOC)。

在这个例子中我们了解到:

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

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

配置写死

最方便的做法,就是把配置直接写在代码里。

// SomeComponent.java
public class SomeComponent {

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

代码写死导致不能方便更改连接的配置,这无法满足实际需求。

依赖注入

为了解决写死的问题,可以把 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 的实例。这样不仅麻烦,而且不能复用数据库连接,造成资源浪费。

管理容器

一个解决办法是,把这些模块装入一个容器,专门用来管理。

// 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之外,它还依赖FileSystemHttpClientHttpCookie。按照上面的方法(工厂模式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 获取 ConnectionFileSystemHttpClientHttpCookie 的实例,从而初始化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 的修改。换句话说,实际上又回到了当初写死代码的情形。

控制反转

为了克服上面的问题,一个解决思路是把 Container 的维护工作交给 框架 (例如Java的Spring,Php 的 Phalcon, JS 的 AngularX) 来完成,即通过一些配置使得框架能 发现 SomeComponent 的依赖对象。

SomeComponent 需要使用这些对象的时候由框架来完成实例化的工作。这样一来,当 SomeComponent 的依赖发生变化时,开发者只需要修改 SomeComponent 和相关依赖的配置,而所有依赖 SomeComponent 的应用程序不需要做修改。这种思路被称为 控制反转,即依赖对象的「控制权」(即对象的生成和销毁) 从开发者手上转移到框架。

以 Springboot 为例,按框架的形式写好 SomeComponent 之后,如果我们需要使用 SomeComponent,大致写法如下:

// Client.java
public class Client{
    
    @Autowired  // 由框架自动生成对象
    private SomeComponent someComponent;

    public Client(SomeComponent someComponent) {
        this.someComponent = someComponent;
    }

    public void useSomeComponent() throws Exception {
        someComponent.doDbTask();
    }
}

说明

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

参考资料

标签 :