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

• 阅读 1085

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

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

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

概念简介

控制反转

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

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

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

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

图片来源于 Martin Fowler 文章

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

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

依赖注入

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

图片来源于 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 配置文件来改变应用的运行行为

服务定位器

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

图片来源于 Martin Fowler 文章

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

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

实际用例

有 Java Spring 框架使用经验的同学对依赖注入应该非常熟悉,IoC 是 Spring 框架的两大基石,另一个是 AOP。依赖注入在 Java 里使用最广泛,这跟 Java 的主攻领域为企业开发有关。企业应用的业务逻辑通常都很复杂,需要借助 IoC 容器来简化大量业务对象的管理。除了 Java,其它语言需要的话也完全可以使用 IoC 容器。因为依赖注入在 Java 里已非常流行,这里就不再阐述,下面以服务端 Python 和客户端 Dart 语言为代表来讲解。每种语言里依赖注入框架和库都有多种选择,这里选择了比较成熟且用法比较简单的。

Python

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

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

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

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

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

用法示例

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

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

图片来源于 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

本文转自 https://blog.jaggerwang.net/simplify-business-objects-management-by-ioc-container/,如有侵权,请联系删除。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Karen110 Karen110
2年前
​一篇文章总结一下Python库中关于时间的常见操作
前言本次来总结一下关于Python时间的相关操作,有一个有趣的问题。如果你的业务用不到时间相关的操作,你的业务基本上会一直用不到。但是如果你的业务一旦用到了时间操作,你就会发现,淦,到处都是时间操作。。。所以思来想去,还是总结一下吧,本次会采用类型注解方式。time包importtime时间戳从1970年1月1日00:00:00标准时区诞生到现在
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
2年前
JS 对象数组Array 根据对象object key的值排序sort,很风骚哦
有个js对象数组varary\{id:1,name:"b"},{id:2,name:"b"}\需求是根据name或者id的值来排序,这里有个风骚的函数函数定义:function keysrt(key,desc) {  return function(a,b){    return desc ? ~~(ak
Wesley13 Wesley13
2年前
unity将 -u4E00 这种 编码 转汉字 方法
 unity中直接使用 JsonMapper.ToJson(对象),取到的字符串,里面汉字可能是\\u4E00类似这种其实也不用转,服务器会通过类似fastjson发序列化的方式,将json转对象,获取对象的值就是中文但是有时服务器要求将传参中字符串中类似\\u4E00这种转汉字,就需要下面 publ
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这