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

本文参考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的构造函数的参数, 称为构造器注入. (其它注入方式可以参考wiki[2])

// 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,639评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,093评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,079评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,329评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,343评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,047评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,645评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,565评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,095评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,201评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,338评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,014评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,701评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,194评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,320评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,685评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,345评论 2 358

推荐阅读更多精彩内容