Spring IoC(一):IoC 与 DI 思想最详解析

157 阅读8分钟

IoC 与 DI 思想理解

在 Spring 中 IoC 与 AOP 是两大最为核心的思想,其中IoC 的全称是 “Inversion of Control”,即控制反转 也被称为依赖注入(Dependency Injection)。

这里的反转和注入具体是指:传统创建对象是由应用程序直接通过 new 关键字实现,而使用 Spring 框架的应用则是通过委托将创建对象的控制权交给了 IoC 容器, 由 IoC容器 控制对象的创建、初始化、注入、销毁等工作。

IoC 不是一种具体的实现技巧,而是一种比较笼统的思想,用于指导框架层面的设计。实现 IoC 的方法有多种:比如依赖注入、模板方法模式、观察者模式。从此观点来说 IoC 并不真正等价于 DI,或者说 DI 只是实现 IoC 的一种方式

IoC 范式揭秘

控制反转是一种带有某些特征的模式——某种控制权由自己变为他人。要确定是否发生控制反转需要确定以下条件:

  1. 控制权是什么?对象创建权?方法调用权?
  2. 使用框架前的控制权在谁手里
  3. 使用框架后的控制权在谁手里

例一:

下面是由Martin Fowler给出的一个 IoC 经典范例,实现的功能是从控制台中收集用户输入数据。

public class InputReaderAndPrint {
    public static void main(String[] args) {
        while (true) {
            BufferedReader userInputReader = new BufferedReader(
                new InputStreamReader(System.in));
            System.out.println("Please enter some text: ");
            try {
                System.out.println(userInputReader.readLine());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

控制权: 控制输入读取并进行输出

框架前控制权所属: main方法,开发人员自己控制输入读取并进行输出

考虑下,上述程序的一个新版本 (引入框架) ,该版本中需要通过图形界面中的文本框来收件用户输入,提交按钮上绑定有一个action监听器。每次点击按钮,输入的文本由监听器收集并打印到面板。

框架后控制权所属: 由事件监听器模型(框架)的控制,调用开发者编写的用于读取和打印用户输入的代码。该框架实际上是一个可扩展的结构,它为开发人员提供了一组注入自定义代码段的切入点。

从更通用的角度来看,由框架定义的每个可调用扩展点(接口、继承的形式)是IoC的一种明确定义的形式(框架控制变为外部控制)

例二:

class MyServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
    	throws ServletException, IOException {
        // developer implementation here
    }
    
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
    	throws ServletException, IOException {
        // developer implementation here
    }
}

控制权: 类方法的调用控制权

使用框架前控制权: 由开发者自己掌握

使用框架Servlet后控制权: HttpServlet类(框架)是完全控制程序的元素,而不是MyServlet这个子类。在由servlet容器创建之后,当收到servlet的GET和POST的HTTP请求,doGet()和doPost()方法中的代码会分别自动调用。

控制权的反转: 子类控制变为容器中模板方法控制(servlet的方法是模板方法模式的实现,稍后我们再深入讨论)。

使用那些通过提供可扩展API,秉承开闭原则的框架时,使用框架的开发人员的角色,最终被归结为定义自己的一组自定义类,即开发人员要么通过实现框架提供的一个或多个接口方式,要么通过继承现有基类的方式。反过来,类的实例却是直接框架进行实例化,并且这些事例是被框架调用的。

该框架调用开发人员,而不是开发人员调用该框架—— Fowler

因此,IoC通常被称为好莱坞原则:

不要打电话给我们,我们会打电话给你。

IoC 实现方式

1、 依赖注入

控制权: 对象的创建方式

框架前控制权所属: 程序开发者自己控制

框架后控制权所属: 外部框架控制,比如 Spring IoC 通过 setter、构造方法等方式注入属性类。

public class DIUserProcessor {
    // 对象创建控制权由自己变为外部注入
    private DIUserQueue userQueue;

    public DIUserProcessor(DIUserQueue userQueue) {
        this.userQueue = userQueue;
    }

    public void process() {
        // process queued users here
    }

    // 可看做外部框架
    public static void main(String[] args) {
        UserFifoQueue fifoQueue = new UserFifoQueue();
        fifoQueue.add(new DIUser("user1"));
        fifoQueue.add(new DIUser("user2"));
        fifoQueue.add(new DIUser("user3"));
        DIUserProcessor userProcessor = new DIUserProcessor(fifoQueue);
        userProcessor.process();
    }
}

2、 模板方法

模板方法模式实现的思想是在一个基类中通过几个抽象方法(也称算法步骤)来定义一个通用的算法,然后让子类提供具体的实现,这样保证算法结构不变。模板方法(processEntity())定义了处理实体的算法,而抽象方法代表了算法的步骤,它们必须在子类中实现。通过继承 EntityProcessor 并实现不同的抽象方法,可以实现若干算法版本。

public abstract class EntityProcessor {

    // 模板方法,一般用 final 修饰,不让子类变更操作
    public final void processEntity() {
        getEntityData();
        createEntity();
        validateEntity();
        persistEntity();
    }

    protected abstract void getEntityData();
    protected abstract void createEntity();
    protected abstract void validateEntity();
    protected abstract void persistEntity();
}

控制权: 方法的调用

使用框架前控制权: 由子类自己掌握

使用框架后控制权: 基类的模板方法调用

这也是 IoC 的典型例子,通过分层结构实现。这种情况下,模板方法只是可调的扩展点的名字,被开发者用来管理自己的一系列实现。

3、 观察者模式

直接通过观察者模式实现IOC,也是一种常见的直观方式。广义上讲,通过观察者实现IOC,与前文通过GUI界面中的action监听器方式类似。但是在使用action监听器情况下,只有在特定的用户事件发生时才会发生调用。观察者模式通常用于在模型视图的上下文中,跟踪模型对象的状态的变迁。

在一个典型的实现中,一到多个观察者绑定到可观察对象(主题),例如通过调用addObserver方法进行绑定。一旦定义了被观察者和观察者之间的绑定,则被观察者状态的变迁都会触发调用观察者的操作。

// 观察者
@FunctionalInterface
public interface SubjectObserver {
    void update();
}
// 被观察者类
public class ObserverUser {
    public static void main(String[] args) {
        ObserverUser observerUser = new ObserverUser("John");
        // 真正发生控制反转的地方,
        observerUser.addObserver(()-> System.out.println("user add Observer "+ observerUser.getName()));
        observerUser.setName("Tom");
    }

    private String name;

    private List<SubjectObserver> observers = new ArrayList<>();

    public ObserverUser(String name) {
        this.name = name;
    }

    public void setName(String name) {
        this.name = name;
        notifyObservers();
    }

    public String getName() {
        return name;
    }

    public void addObserver(SubjectObserver observer) {
        observers.add(observer);
    }

    private void notifyObservers(){
        observers.stream().forEach(observer -> observer.update());
    }
}

控制权: 观察者的调用

使用框架前控制权: 开发者自己掌握

使用框架后控制权: 基于被观察者状态的变更,而不是自己控制调用。

观察者模式下,主题就是起到”框架层“的作用,它完全主导何时何地去触发谁的调用。观察者的主动权被外放,因为观察者无法主导自己何时被调用(只要它们已经被注册到某个主题中的话)。这意味着,实际上我们可以发现控制被反转的”事发地“ –– 当观察者绑定到主题时:

user.addObserver(()-> System.out.println("user add Observer "+ user.getName()));

观察者模式(或GUI驱动环境中的action监听器)是实现IoC的一种非常简单的方式。正是以这种分散式设计软件组件的形式,使得控制得以发生反转。

IoC 思想总结

尽管控制反转普遍存在于 Java 的生态系统中,特别是很多框架普遍采用了依赖注入,但对于多数开发者来说,这个模式仍然很模糊,对其应用也受限于依赖注入。其实控制反转有很多种形式,具体要看控制权是什么,又是怎样反转的。

  • 依赖注入:从客户端获得类的依赖关系的控制权不再存在于这些类中。它存由底层的注入器 / DI 框架来处理。
  • 观察者模式:当主体发生变化时,控制从观察者传递到主体。
  • 模板方法模式:控制发生在定义模板方法的基类中,而不是实现算法步骤的子类中。

DIP-- 依赖反转原则

介绍完 DI(依赖注入),再看一个与IoC 思想相似的设计原则——Dependency Inversion Priciple,即依赖反转原则。其定义如下:

High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions.

高层模块(high-level modules)不要依赖低层模块(low-level)。高层模块和低层模块应该通过抽象(abstractions)来互相依赖。除此之外,抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)依赖抽象(abstractions)

高层模块和低层模块:在调用链上,调用者属于高层,被调用者属于低层。

在平时的业务代码开发中,高层模块依赖底层模块是没有任何问题的。实际上, 这条原则主要还是用来指导框架层面的设计。如:Tomcat (高层模块)调用Web应用程序(低层模块),二者之间没有直接依赖关系,只是统一依赖同一个抽象——Servlet规范。

Spring 中 IoC 与 DI 并不是完全相同的概念,IoC 是思想、DI 是实现。从思想上来说,IoC 等价于 DIP 而不是 DI。

参考文章:

1、《设计模式之美》

2、 coyee.com/article/121…