本文已授权 InfoQ 转载,其它网站如有转载,请注明出处。

概念简介

控制反转

控制反馈思想很早就有了,软件设计专家 Martin Fowler 在 2004 年编写的一篇文章 Inversion of Control Containers and the Dependency Injection pattern 里对其进行了总结,对控制反转、依赖注入这些概念完全不了解的可以先阅读此文。这里不再赘述其中的内容,只是阐述一下个人对这些概念的理解。

在正常情况下,当前对象会自己负责创建其依赖的所有对象,也就是当前对象为控制方。而在控制反转情况下,当前对象会以某种方式自动获得其依赖的所有对象,就像是被控制了一样。这个控制方现在就是 IoC 容器,前提是被创建的对象允许以某种方式由外部注入其依赖对象。

按照自动获得依赖对象的方式的不同,控制反转思想的实现可分为依赖注入和服务定位器(Service Locator)两种模式。两者并不互斥,通常会结合起来使用。不过依赖注入的使用场景要远多于服务定位器,这也是通常只把它跟控制反转一起提及的原因。依赖注入能够解耦组件之间的关系,从而使得组件使用起来更简单,也变得更加通用,同时还能简化应用结构。下面将讲解两种模式的实现原理、优缺点,以及它们之间的区别。

injection-image-1
图片来源于 Martin Fowler 文章

上图中,MovieLister 对象用来检索电影,它依赖实现了 MovieFinder 接口的 MovieFinderImpl 对象来加载存储在外部的电影数据。外部存储电影数据的方式有很多种,为了能够支持不同的存储方式,MovieLister 只要求其依赖的存储对象实现了 MovieFinder 接口即可。MovieLister 内部会在需要的时候自己创建 MovieFinderImpl 对象,这样它就会同时依赖 MovieFinder 接口和 MovieFinderImpl 实现类。

简单场景下这种方式没什么问题,但如果放到像企业应用这样拥有大量业务对象的应用里就不合适了。各个类之间紧密耦合,每个类除了直接依赖类,还会依赖这些依赖类的依赖类,照此往复,类之间的关系就会变得异常复杂。并且创建对象的代码充斥在应用里的各个角落,如果类的构造函数有变动,那么需要修改用到该类的各个地方。那么依赖注入和服务定位器是如何解决这个问题的了?

依赖注入

injection-image-2
图片来源于 Martin Fowler 文章

在依赖注入模式里多了一个 Assembler,它承接了 MovieFinderImpl 对象的创建工作,现在 MovieLister 只依赖 MovieFinderImpl 接口,跟具体的实现类没有关系了。Assembler 负责所有对象的创建,包括 MovieLister。在创建 MovieLister 的时候发现它需要一个实现了 MovieFinder 接口的对象,那么它会自动创建一个 MovieFinderImpl 对象并注入给 MovieLister。这样一来,各个类之间就完全解耦了,它们互不知晓,只需要 Assembler 清楚它们之间的关系就可以。对象构造方式如果有变动,只需要修改 Assembler 一处。进行单元测试也变得更容易,也只需要在 Assembler 里构造对象的时候把外部依赖对象替换为模拟对象即可。

下面是有关依赖注入的一些术语:

  • 假设 A 对象依赖 B 对象,那么 A 称为 client,而 B 称为 service
  • 负责创建对象以及为其注入依赖对象的代码称为依赖注入器(Dependency Injector)或 IoC 容器

给对象注入其依赖对象有多种方式:

  • 构造函数或者初始化方法(比如 Python 类的 __init__)注入,依赖对象通过函数参数传入,这是用得最多的一种
  • 属性注入,通过设置对象的成员或属性来注入
  • 方法注入,通过调用对象方法来注入

依赖注入有下面一些原则需要遵循:

  • client 委托依赖注入器来注入其依赖对象
  • client 并不知道如何创建 service,只知道 service 的接口,同时 service 也不知道自己被哪些 client 使用
  • 依赖注入器知道如何创建 client 和 service,以及它们之间的依赖关系
  • client 和 service 对依赖注入器一无所知

使用依赖注入能够带来以下好处:

  • 把控应用结构
  • 减少应用内组件之间的连接
  • 增加代码复用
  • 增加代码可测试性
  • 增加代码可维护性
  • 无需重新构建即可重新配置应用,比如 Java 里通过修改依赖注入 XML 配置文件来改变应用的运行行为

服务定位器

injection-image-3
图片来源于 Martin Fowler 文章

相比于依赖注入模式,服务定位器模式多了一个 ServiceLocator。相比于依赖注入主动注入依赖对象,这种模式下对象需要主动从 ServiceLocator 里去获取其各个依赖对象。服务定位器相当于一个注册表,它把散落在各个地方的对象集中到了一起。服务定位器会返回特定类型的对象,那如果需要其它实现类的对象怎么办?这种情况可以使用多个服务定位器,或者多个派生子类。不同的运行环境使用不同的服务定位器,比如运行单元测试时使用返回模拟对象的服务定位器。因为服务定位器的逻辑很简单,维护多个的成本完全可以接受。

看起来服务定位器好像并没有依赖注入那么有用,但它也有其使用场景,并且在某些场景下还是必需的。在不像 Java 那样的严格面向对象语言里,比如 Go、Python,许多使用对象的地方并不在类中,比如 Web 请求处理器通常为一个函数。这个时候依赖注入就没法派上用场了,只能使用服务定位器。

实际用例

下面是依赖注入在各种语言里的实际使用例子。每种语言里提供依赖注入的框架和库都有多种选择,这里选择了比较成熟,并且用法比较简单的。

Python

Dependency Injector 是一个 Python 依赖注入微框架,性能高效(C 扩展实现),用法简单。Dependency Injector 里只有两个概念,Provider 和 Container。

Provider 用来定义获取对象的策略,可以使用下面这些策略:

  • Callable - 可调用对象,支持位置和关键字参数注入
  • Factory - 工厂,每次调用将返回一个新对象,支持位置和关键字参数注入,以及属性注入
  • Singleton - 单例,每次调用会返回同一个对象,支持位置和关键字参数注入,以及属性注入
  • Object - 对象,原样返回对象
  • Configuration - 配置,用于定义容器时还无法确定的对象,需要在创建容器的时候作为参数传入

Container 用来存放 provider,主要用来对 provider 进行分组。有两种容器:

  • DeclarativeContainer - 声明式容器,大多数情况下的选择,适用于 provider 可以提前确定的
  • DynamicContainer - 动态容器,在运行时动态创建各个 provider

用法示例

下面通过一个简单的汽车例子来学习 Dependency Injector 的基本用法。

dependency-injection-car
图片来源于 Dependency Injector 文档

每辆汽车都有一个引擎,引擎分为汽油的、柴油的和电动的。不使用依赖注入的实现代码如下。

class Engine:
    """引擎基类,相当于其它语言里的接口
    """


class GasolineEngine(Engine):
    """汽油引擎
    """


class DieselEngine(Engine):
    """柴油引擎
    """


class ElectroEngine(Engine):
    """电动引擎
    """


class Car:
    """汽车
    """

    def __init__(self, engine):
        """初始化函数,可注入引擎对象
        """
        self._engine = engine


if __name__ == '__main__':
    gasoline_car = Car(GasolineEngine())
    diesel_car = Car(DieselEngine())
    electro_car = Car(ElectroEngine())

可以看到,为了创建不同类型的汽车,需要自己创建对应的引擎并通过初始化函数参数注入进去。再来看一下使用依赖注入框架的版本。

import dependency_injector.containers as containers
import dependency_injector.providers as providers


class Engines(containers.DeclarativeContainer):
    """引擎 IoC 容器
    """

    gasoline = providers.Factory(GasolineEngine)
    diesel = providers.Factory(DieselEngine)
    electro = providers.Factory(ElectroEngine)


class Cars(containers.DeclarativeContainer):
    """汽车 IoC 容器
    """

    gasoline = providers.Factory(Car, engine=Engines.gasoline)
    diesel = providers.Factory(Car, engine=Engines.diesel)
    electro = providers.Factory(Car, engine=Engines.electro)


if __name__ == '__main__':
    gasoline_car = Cars.gasoline()
    diesel_car = Cars.diesel()
    electro_car = Cars.electro()

使用 Dependency Injector,需要为引擎和汽车分别创建一个 IoC 容器,当然也可以合成一个。IoC 容器负责对象的创建和组装,里面定义了各种对象的 provider,调用 provider 将返回对应类型的对象。需要注入的依赖对象也是通过 provider 来提供的,在创建对象的时候框架会自动调用 provider 来获取依赖对象。

实际用例

下面所讲的实例来自于 GitHub 项目 Sanic in Practice

weiguan/container.py

import logging
import asyncio

from dependency_injector import providers, containers
from aiomysql.sa import create_engine, Engine
from aioredis import create_redis_pool, Redis

from .utils import SingletonMeta
from .dependencies import MessageChannel, ...
from .services import MessageService, ...
from .cli.commands import RootCommand, ...


class _Container(containers.DeclarativeContainer):
    """IoC 容器
    """

    config = providers.Configuration('config')
    db = providers.Configuration('db')
    cache = providers.Configuration('cache')

    app_logger = providers.Callable(logging.getLogger, name='app')

    message_channel = providers.Singleton(
        MessageChannel, config=config, cache=cache)
    post_repo = providers.Singleton(PostRepo, db=db)
    ...

    message_service = providers.Singleton(
        MessageService, config=config, channel=message_channel)
    user_service = providers.Singleton(
        UserService, config=config, user_repo=user_repo,
        user_follow_repo=user_follow_repo)
    ...

    model_command = providers.Factory(ModelCommand, config=config)
    ...


class Container(metaclass=SingletonMeta):
    """单例 IoC 容器
    """

    def __init__(self, config: dict = None, log_config: dict = None):
        self.on_init = asyncio.create_task(self._init(config, log_config))

    async def _init(self, config: dict, log_config: dict):
        """异步初始化
        """

        logging.config.dictConfig(log_config)

        db: Engine = await create_engine(...)

        cache: Redis = await create_redis_pool(...)

        self.container = _Container(config=config, db=db, cache=cache)

        await self.message_channel.on_init

    @property
    def config(self) -> dict:
        return self.container.config()

    @property
    def db(self) -> Engine:
        return self.container.db()

    @property
    def cache(self) -> Redis:
        return self.container.cache()

    @property
    def app_logger(self) -> logging.Logger:
        return self.container.app_logger()

    @property
    def message_channel(self) -> MessageChannel:
        return self.container.message_channel()

    @property
    def post_repo(self) -> PostRepo:
        return self.container.post_repo()

    ...

    @property
    def message_service(self) -> MessageService:
        return self.container.message_service()

    @property
    def user_service(self) -> UserService:
        return self.container.user_service()

    ...

    @property
    def model_command(self) -> ModelCommand:
        return self.container.model_command()

    ...

上面定义了两个 IoC 容器,其中 _Container 是真正的 IoC 容器,但由于其继承了 DeclarativeContainer 基类,无法通过元类方式实现单例模式,因此又定义了一个包装类 ContainerContainer 通过元类方式实现了单例模式,其它地方使用它来获取对象,相当于是一个服务定位器。为了方便其它地方获取对象,Container 类定义了一系列的 getter 方法,并且注明了返回类型,以便编写代码时可以得到类型提示。另外,创建 IoC 容器需要执行一些异步的初始化工作,由于 Python 类初始化方法 __init__ 不支持异步操作,这里使用了一个单独的 _init 方法来完成容器的创建和初始化。该方法通过一个 on_init 异步任务来执行,使用者需要等待该异步任务完成后才能使用容器。

首先在应用入口里执行 container = Container(config, log_config) 来创建容器,并执行 await container.on_init 来等待容器初始化完成,然后使用者(比如请求处理器里)就可以使用类似 Container().user_service 这样的调用来获得需要的对象。可以看到这里同时使用了依赖注入和服务定位器两种模式,因为请求处理器为一个函数,无法为其注入依赖对象。

Dart

随着 Flutter 跨平台 UI 框架的流行,其开发语言 Dart 也跟着火了起来。大部分客户端应用的业务逻辑都不会太复杂,也没有太多外部依赖,因此用不上依赖注入。但如果确实需要,也完全可以使用。同样在 Dart 语言里也有多种依赖注入框架可选,这里选择了 Injector,它的用法也很简单。

用法示例

仍然以前面的汽车例子为例,在 Dart 语言里使用 Injector 的实现版本如下。

import 'package:injector/injector.dart';
import 'package:meta/meta.dart';

abstract class Engine {}

class GasolineEngine extends Engine {}

class DieselEngine extends Engine {}

class ElectroEngine extends Engine {}

class Car {
  final Engine engine;

  Car({@required this.engine});
}

void main() {
  Injector injector = Injector.appInstance;

  injector.registerDependency<GasolineEngine>((_) => GasolineEngine());
  injector.registerDependency<DieselEngine>((_) => DieselEngine());
  injector.registerDependency<ElectroEngine>((_) => ElectroEngine());

  injector.registerDependency<Car>(
      (_) => Car(engine: injector.getDependency<GasolineEngine>()),
      dependencyName: "gasoline");
  injector.registerDependency<Car>(
      (_) => Car(engine: injector.getDependency<DieselEngine>()),
      dependencyName: "diesel");
  injector.registerDependency<Car>(
      (_) => Car(engine: injector.getDependency<ElectroEngine>()),
      dependencyName: "electron");

  injector.getDependency<Car>(dependencyName: "gasoline");
  injector.getDependency<Car>(dependencyName: "diesel");
  injector.getDependency<Car>(dependencyName: "electron");
}

Injector 就是 IoC 容器,通过其静态成员 appInstance 提供了一个单例对象。通过调用容器的 registerDependency 方法来注册某种类型对象的创建函数,如果需要实现单例模式,那么可以使用 registerSingleton 方法。注册的时候还可以提供一个依赖名字 dependencyName,用来区分同一类型对象的不同构造方式。比如示例里的三种汽车,类型都是 Car,但它们的构造方式并不一样。注册好对象之后,使用者通过调用 getDependency 来获取指定类型的对象。如果该类型的对象注册了多种构造方式,那么还需要指定 dependencyName

实际用例

下面再来看一个实际的例子,代码截取自 GitHub 项目 Flutter in Practice

lib/weiguan/container.dart

...

class WgContainer {
  static WgContainer _instance;

  final Injector _injector = Injector();
  WgConfig _config;
  Future<void> onReady;

  factory WgContainer([WgConfig config]) {
    if (_instance == null) {
      _instance = WgContainer._(config);
    }

    return _instance;
  }

  WgContainer._(WgConfig config) {
    _config = config;

    onReady = Future(() async {
      _injectTheme();

      _injectLogger();

      await _injectPackageInfo();

      ...
    });
  }

  WgConfig get config {
    return _config;
  }

  void _injectTheme() {
    _injector.registerSingleton<WgTheme>((injector) {
      return WgTheme();
    });
  }

  WgTheme get theme {
    return _injector.getDependency<WgTheme>();
  }

  void _injectLogger() {
    ...

    _injector.registerSingleton<Logger>((injector) {
      return Logger('app');
    }, dependencyName: 'app');
    _injector.registerSingleton<Logger>((injector) {
      return Logger('action');
    }, dependencyName: 'action');
    _injector.registerSingleton<Logger>((injector) {
      return Logger('api');
    }, dependencyName: 'api');
  }

  Logger get appLogger {
    return _injector.getDependency<Logger>(dependencyName: 'app');
  }

  Logger get apiLogger {
    return _injector.getDependency<Logger>(dependencyName: 'api');
  }

  Logger get actionLogger {
    return _injector.getDependency<Logger>(dependencyName: 'action');
  }

  Future<void> _injectPackageInfo() async {
    final packageInfo = await PackageInfo.fromPlatform();
    _injector.registerDependency<PackageInfo>((injector) {
      return packageInfo;
    });
  }

  PackageInfo get packageInfo {
    return _injector.getDependency<PackageInfo>();
  }
  
  ...
}

上面的 WgContainer 对 Injector 做了一层包装,因为需要对容器进行配置并执行一些初始化工作。Dart 语言里面实现单例模式还是非常简单的,使用 factory 工厂构造函数即可。由于初始化工作为异步,因此使用了一个 onReady Future 对象来在初始化完成的时候通知调用者。为了方便使用者从容器里获取对象,对每种类型的对象都定义了一个 getter 方法。

在应用入口里使用 final container = WgContainer(WgConfig()) 来创建容器,这时需要传入应用配置,并且还需要执行 await container.onReady 来等待容器初始化完成。然后就可以在其它地方使用类似 WgContainer().theme 这样的方式来从容器里获取对象了。

参考资料

  1. Inversion of Control Containers and the Dependency Injection pattern
  2. Dependency Injector
  3. Sanic in Practice
  4. Injector
  5. Flutter in Practice