首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

使用 IoC 容器来简化业务对象的管理

有过复杂业务应用编写经验的开发人员都知道业务对象的创建是一件比较麻烦的事儿。这些应用中存在着大量的业务对象,它们之间有着复杂的依赖关系,导致模块之间很容易出现循环依赖。此外,有些对象还有单例要求,依赖之间还有顺序要求,这些更加重了问题的严重性。这种情况下就需要有一种手段来简化业务对象的管理,包括创建和获取,IoC(Inversion of Control)容器正是为此而生。IoC 容器要求被管理的对象支持依赖注入(Dependency Injection),以便给这些对象注入其依赖的对象。本文先对控制反转和依赖注入的概念作简单介绍,然后重点讲解它们在各种语言里的实际用法。

概念简介

控制反转

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

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

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

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

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

依赖注入

在依赖注入模式里多了一个 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 配置文件来改变应用的运行行为

服务定位器

相比于依赖注入模式,服务定位器模式多了一个 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 的基本用法。

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

代码语言:javascript
复制
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())

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

代码语言:javascript
复制
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

代码语言:javascript
复制
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 基类,无法通过元类方式实现单例模式,因此又定义了一个包装类 Container。Container 通过元类方式实现了单例模式,其它地方使用它来获取对象,相当于是一个服务定位器。为了方便其它地方获取对象,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 的实现版本如下。

代码语言:javascript
复制
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

代码语言:javascript
复制
...

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 这样的方式来从容器里获取对象了。

原文链接

本文转载已获授权,原文链接:使用 IoC 容器来简化业务对象的管理

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/wILXgbkJ9juQoMxsa3dX
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券