写点什么

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

  • 2019-11-06
  • 本文字数:7363 字

    阅读完需:约 24 分钟

使用 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 的基本用法。



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


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 containersimport 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 loggingimport asyncio
from dependency_injector import providers, containersfrom aiomysql.sa import create_engine, Enginefrom aioredis import create_redis_pool, Redis
from .utils import SingletonMetafrom .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 的实现版本如下。


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

原文链接

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


2019-11-06 10:322245

评论 1 条评论

发布
用户头像
Dart 实例部分的代码错乱了,也没有正确高亮,感兴趣的可以查看原文。https://blog.jaggerwang.net/clean-architecture-in-practice/
2019-11-21 18:46
回复
没有更多了
发现更多内容

深度分享|中小银行如何实现数字化转型,建设智能营销新体系?

索信达控股

大数据 数字化转型 金融 银行 营销数字化

麦肯锡最新报告 | 开发者速率成为企业增长助推剂

LigaAI

SaaS

面试37次,16个offer上岸,总结了一些面试心得

北游学Java

面试 offer

阿里的互联网三高架构是真的牛!腾讯百度根本模仿不来

Java架构师迁哥

如何快速构建嵌入式全栈知识体系?

博文视点Broadview

打破固有思维(十八)

Changing Lin

5月日更

一文带你认识MindSpore新一代分子模拟库SPONGE

华为云开发者联盟

神经网络 mindspore 新一代分子模拟库 SPONGE 分子结构

ASP.NET Core整合Zipkin链路跟踪

yi念之间

Mybatis-Plus的应用场景及注入SQL原理分析

vivo互联网技术

源码分析 mybatis

书单 | 职场办公类私藏好书,轻松Get职场必备技能

博文视点Broadview

iOS 面试策略之系统框架-设计模式

iOSer

ios 并发编程 tableView

李开复、张亚勤、吴恩达…国际大咖给你讲解AI知识

博文视点Broadview

Flink 实时计算在微博的应用

Apache Flink

flink

解密华为云FusionInsight MRS新特性:一架构三湖

华为云开发者联盟

数据湖 云原生 华为云 FusionInsight MRS TechWave

Docker挂了,数据如何找回

运维研习社

Docker 运维 数据恢复 5月日更

dubbo的前世今生

捉虫大师

dubbo

强化学习落地:竞态场景下基于锁机制的闲置端口查用

行者AI

强化学习

超详细的JQuery的 DOM操作,一篇就足够!

华为云开发者联盟

html 大前端 DOM 函数 JQuery框架

一种基于实时分位数计算的系统及方法

百度Geek说

云计算 大前端 云服务

网页端IM通信技术快速入门:短轮询、长轮询、SSE、WebSocket

JackJiang

websocket 消息推送 即时通讯 IM

架构是什么?空中楼阁?不切实际?

Java架构师迁哥

当当购书优惠码又可以限时抢啦(热卖新书等你来)

博文视点Broadview

云智慧发布《智能业务运维》2021年刊 邀您共览数字化运维全景象

云智慧AIOps社区

AIOPS 智能运维

☕【JVM 技术之旅】带你重塑对类加载机制的认识

洛神灬殇

JVM Java虚拟机 类加载器 原理分析 5月日更

Java程序员想要拿到50W以上年薪,哪个技术是必备的?

Java架构师迁哥

联邦学习在视觉领域的应用,揭秘AI人工智能创新应用奖获奖案例

博文视点Broadview

树莓派上的 K8S 集群挂了,怎么办?

百度开发者中心

百度 技术 经验分享

做开发,这几种锁机制你不得不了解一下

华为云开发者联盟

读写锁 自旋锁 互斥锁 优先锁

毕业设计So Easy:基于Java语言西餐厅点餐系统

不脱发的程序猿

Java 开源 Java语言西餐厅点餐系统 毕业设计

云小课 | 华为云KYON之L2CG

华为云开发者联盟

虚拟私有云 华为云 大二层网络 KYON企业级云网络 L2CG

软硬件融合新时代——让软件够灵活,硬件够高效,鱼和熊掌可兼得

博文视点Broadview

使用 IoC 容器来简化业务对象的管理_文化 & 方法_jaggerwang_InfoQ精选文章