IoC 和 DI 的初级整理

面向对象编程(OOP)不仅是一种设计编程语言的新方式,也是一种完全不同的程序设计方式。
本文解释了如何使用Smalltalk设计系统。由于软件重用是面向对象编程的主要动机之一,本文还阐述了如何开发可重用的类。

什么是IoC?

控制反转(Inversion of Control, IoC)是一种软件设计模式,程序员编写的程序会受到可重用库的流程控制。

在传统的编程中,程序的流程是由程序员编写的代码调用外部库来实现的。然而,在应用了控制反转的结构中,外部库的代码会反过来调用程序员编写的代码。

IoC的概念最早出现在1988年Ralph E. Johnson和Brian Foote撰写的经典论文《Designing Reusable Classes》中。这篇论文旨在提高代码的可重用性,并提出了OOP的几个核心原则:

  • 多态性(Polymorphism): 不同对象可以通过相同的消息进行交互的特性。
  • 协议(Protocol): 对象需要提供的一组服务或方法集,即接口的明确定义。
  • 继承(Inheritance): 新类可以继承现有类的特性和行为,并对其进行扩展或修改的机制。
  • 抽象类(Abstract Classes): 不提供具体实现,而是定义子类必须实现的接口或基本结构的类。

(需要注意的是,这些定义与我们现在使用的OOP概念有很大不同。这反映了OOP从Alan Kay的概念到后来Booch的概念演变的过程。)

框架的定义:

在这篇论文中,作者将框架定义为以下公式:

框架 = 组件 + 控制反转(IoC)

这里,IoC被解释为框架控制应用程序流程的方式。这篇论文被认为是现代框架概念的奠基之作,也是首次系统化地定义IoC的论文。

IoC的核心思想是,与传统程序由用户直接控制流程不同,IoC允许外部控制流程,而用户只需实现必要的部分。(论文中将这个“外部”称为框架。)

(尽管IoC一词通常被认为起源于1988年的《Designing Reusable Classes》,但在那之前已经有人在使用类似的概念。这篇论文只是正式确立了这一术语。)

// 传统方式 - 直接在类 B 内部创建 A 的实例
class B {
    public void doSomethingWithA() {
        A a = new A(); // B 直接创建 A 实例
        a.action();
    }
}

// A 类(依赖提供者)
class A {
    public void action() {
        System.out.println("A 的动作");
    }
}

// 测试代码
public class Main {
    public static void main(String[] args) {
        B b = new B(); // 创建 B 的实例
        b.doSomethingWithA(); // 直接调用方法
    }
}

简单示例:

在传统的编程方式中,对象在需要时自行创建或生成其他对象。也就是说,程序的流程由对象自身主导。

而在应用了IoC的情况下,对象不会自行创建或查找所需的对象。相反,外部容器负责创建所需的对象并设置对象之间的依赖关系。

通过这种方式,对象可以专注于其核心逻辑。

一个简单的例子是IoC中最基础的形式之一:构造函数注入。

// A 类(依赖提供者)
class A {
    public void action() {
        System.out.println("A 的动作");
    }
}

// B 类(依赖消费者) - 通过构造函数注入 A
class B {
    private A a; // 依赖项

    // 通过构造函数注入 A
    public B(A a) {
        this.a = a;
    }

    public void doSomethingWithA() {
        a.action(); // 使用注入的 A 实例
    }
}

// 外部容器
public class Main {
    public static void main(String[] args) {
        A a = new A(); // 由容器创建 A 实例
        B b = new B(a); // 由容器创建 B 实例,并注入 A

        b.doSomethingWithA(); // 执行方法
    }
}

IoC的核心概念:

为了更好地理解IoC,我们先介绍其核心概念。

“别打电话给我们,我们会打给你。” —— Sugarloaf, 1974

IoC的核心概念可以总结如下:

好莱坞原则(Hollywood Principle):

这一原则最早出现在1983年Richard E. Sweet撰写的《The Mesa Programming Environment》论文中,后来由现代OOP之父之一John Vlissides在《C++ Report》中引用,并由Martin Fowler推广而广为人知。

这一原则源自好莱坞电影公司的做法:演员不需要主动联系导演,而是留下联系方式等待导演的“回电”。

简单来说:

  1. 演员(调用者)不会主动联系导演(框架)请求出演机会,而是由导演根据需要选择合适的演员并发出“选角电话”。这强调了控制权不在演员手中,而是在导演手中。

总之,IoC意味着控制的反转,即将程序的流程控制从开发者转移到框架。开发者遵循框架提供的规则和API编写代码,而框架则管理对象的生命周期和交互。

IoC的实现模式:

IoC的实现模式可以从两个主要方面来看:

  1. 回调(Callback): 在事件处理、GUI框架中常见。
  2. 模板方法模式(Template Method Pattern): 上层类定义整个算法流程,下层类可以覆盖部分步骤。
  3. 发布-订阅模式(Publisher-Subscriber Pattern): 当主题(Subject)触发某个事件时,通知注册的发布者。

IoC容器(IoC Container):

实际实现和管理IoC的角色是IoC容器。IoC容器的主要功能包括:

  1. 对象的创建与管理: 创建应用程序所需的对象并管理其生命周期(创建、初始化、销毁等)。
  2. 依赖注入(Dependency Injection, DI): 分析对象之间的依赖关系,并将所需的对象注入到其他对象中。
  3. 对象的检索与提供: 根据需要检索并提供容器中注册的对象。

IoC的优点包括:

  • 降低耦合度: 减少对象之间的依赖,提高代码的可重用性。
  • 增强灵活性: 可以通过配置文件或其他机制轻松更改依赖关系。
  • 简化代码: 减少了对象创建和依赖管理的代码,使代码更加简洁。
  • DI(依赖注入)是什么?

    读者可能对突然出现的“依赖注入(DI)”感到困惑。接下来我们将解释DI的概念。​​​​​​

由于IoC的范围过于广泛,可能会让人难以理解。因此,伟大的程序员Martin Fowler在一篇经典文章《Inversion of Control Containers and the Dependency Injection pattern》(2004)中整理了IoC的各种形式,并定义了依赖注入(DI)作为IoC的一种特定模式。

控制反转(IoC)"这一术语过于宽泛,容易让人感到困惑。
因此,经过与众多IoC倡导者的广泛讨论,最终决定采用“依赖注入(Dependency Injection, DI)”这一名称

接着,Martin Fowler在文章中举例说明了DI的三种具体形式:

依赖注入的三种形式:

// A 类(依赖提供者)
class A {
    public void action() {
        System.out.println("A 的动作");
    }
}

// B 类(依赖消费者) - 通过构造函数注入 A
class B {
    private A a; // 依赖项

    // 通过构造函数注入 A
    public B(A a) {
        this.a = a;
    }

    public void doSomethingWithA() {
        a.action(); // 使用注入的 A 实例
    }
}

// 外部容器
public class Main {
    public static void main(String[] args) {
        A a = new A(); // 由容器创建 A 实例
        B b = new B(a); // 由容器创建 B 实例,并注入 A

        b.doSomethingWithA(); // 执行方法
    }
}
  1. 构造函数注入(Constructor Injection): 在对象创建时通过构造函数注入依赖(Spring和DI容器中常用)
    // A 类(依赖提供者)
    class A {
        public void action() {
            System.out.println("A 的动作");
        }
    }
    
    // B 类(依赖消费者) - 通过 Setter 方法注入 A
    class B {
        private A a; // 依赖项
    
        // Setter 方法用于注入 A
        public void setA(A a) {
            this.a = a;
        }
    
        public void doSomethingWithA() {
            if (a != null) {
                a.action(); // 使用注入的 A
            } else {
                System.out.println("A 未被注入");
            }
        }
    }
    
    // 外部容器
    public class Main {
        public static void main(String[] args) {
            A a = new A(); // 容器创建 A 实例
            B b = new B(); // 容器创建 B 实例
            b.setA(a); // 通过 Setter 方法注入 A
    
            b.doSomethingWithA(); // 执行方法
        }
    }
    
  2. Setter注入(Setter Injection): 通过Setter方法注入依赖(常用于XML配置)。
    // 依赖注入接口
    interface InjectableA {
        void injectA(A a);
    }
    
    // A 类(依赖提供者)
    class A {
        public void action() {
            System.out.println("A 的动作");
        }
    }
    
    // B 类(依赖消费者) - 通过接口方法注入 A
    class B implements InjectableA {
        private A a;
    
        // 通过接口方法注入 A
        @Override
        public void injectA(A a) {
            this.a = a;
        }
    
        public void doSomethingWithA() {
            if (a != null) {
                a.action(); // 使用注入的 A
            } else {
                System.out.println("A 未被注入");
            }
        }
    }
    
    // 外部容器
    public class Main {
        public static void main(String[] args) {
            A a = new A();      // 容器创建 A 实例
            B b = new B();      // 容器创建 B 实例
            b.injectA(a);       // 通过接口方法注入 A
    
            b.doSomethingWithA(); // 执行方法
        }
    }
    
  3. 接口注入(Interface Injection): 通过接口注入依赖(作者常用)。

注入方式

优点

缺点

使用场景

构造函数注入

保证不可变性,对象完全初始化

循环依赖难以处理

必须依赖的情况

Setter注入

支持可选依赖,易于解决循环依赖

难以保证不可变性,对象状态可能改变

非必要依赖的情况

接口注入

限制特定对象的注入

维护困难,代码可能变得复杂

强制特定注入模式

总结:

  • IoC(控制反转): 将控制流委托给外部的上层概念。可以通过框架、回调、事件等方式实现。
  • DI(依赖注入): 实现IoC的下层概念,通过外部注入对象之间的依赖关系。

最后,IoC和DI不仅仅是技术工具,更是反映软件设计哲学的概念。通过降低对象之间的耦合度,提高代码的灵活性和测试便利性,它们可以帮助我们设计和开发更优秀的软件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值