东亚银行、岚图汽车带你解锁 AIGC 时代的数字化人才培养各赛道新模式! 了解详情
写点什么

使用 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:322138

评论 1 条评论

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

使用AWS CodePipeline自动部署项目到EC2

王坤祥

亚马逊云 亚马逊云科技 EC2 CodePipeline CodeDeploy

go-mongox:简单高效,让文档操作和 bson 数据构造更流畅

陈明勇

Go golang mongodb 开源 go mongo

「冰点还原精灵」Deep Freeze for mac 系统还原工具

加油,小妞!

Deep Freeze 系统还原

实例讲解数据库的定义重载函数

华为云开发者联盟

数据库 后端 华为云 华为云GaussDB 华为云开发者联盟

推动OpenHarmony在AIDC行业落地,优博讯的技术积累与实践

Geek_2d6073

市场行情回暖、利好月来袭,Web3 广告业领头羊 Verasity 或迎爆发

鳄鱼视界

文心一言 VS 讯飞星火 VS chatgpt (135)-- 算法导论11.3 1题

福大大架构师每日一题

福大大架构师每日一题

Mac电脑压缩解压工具WinZip激活最新版

胖墩儿不胖y

Mac软件 压缩工具 解压缩软件

“枫”景独美时,在华为天气发现观赏指南

最新动态

数据库操作入门:PyMongo 和 MongoDB 的基本用法

小万哥

Python 程序员 软件 后端 开发

高效使用 PyMongo 进行 MongoDB 查询和插入操作

小万哥

Python 程序员 软件 后端 开发

以终为始,依托度量指标,多种工具全面提升研发效能

之家技术

工具 团队 效能 AIGC 释产能

Milvus 上新!全新 Range Search 功能,可精准控制搜索结果

Zilliz

Milvus Zilliz 向量数据库

Databend 与海外某电信签约:共创海外电信数据仓库新纪元

Databend

世界杯、NBA赛事直播视频源数据接口获取,开发体育直播平台搭建行动方案

软件开发-梦幻运营部

FTP与文件管理工具 ForkLift 4 激活最新版

mac大玩家j

文件传输工具 ftp工具

Macos硬件监测和系统维护工具:TechTool Pro for mac

加油,小妞!

系统维护 mac系统维护软件 TechTool Pro

多行业用户齐聚,2023 IoTDB 用户大会详细议程更新!

Apache IoTDB

一文带你了解TypeScript 函数

Aion

typescript Vue 前端

Kyligence 入选 Gartner® 2023 客户之声报告,高分获评“卓越表现者”

Kyligence

数据分析 指标平台

10w+播放!自动化办公,用到了哪些实用的Python技术?

程序员晚枫

Python 编程 自动化办公

概念回顾: API 和 API 互联

NGINX开源社区

API 协议 API 类型 API 策略 API 架构 API使用现状

玩转 Cgroup 系列之三:挑战手动管理 Cgroup

小猿姐

cpu 资源管理 Cgroup

选人与育人,孰先孰后?

凌晞

团队管理

typhonjs-escomplex 代码可读性 & 可维护度探索

南城FE

JavaScript 前端开发 代码质量 代码规范

企业APP软件定制开发的关键步骤

Geek_16d138

软件开发 app定制开发

OpenHarmony开发者论坛正式上线,盖楼赢惊喜好礼~

OpenHarmony开发者

OpenHarmony

释产能,论研效提升之路

之家技术

效率 团队 资源 效能 释产能

保驾“双十一” 博睿数据助力电商零售迎高峰无烦忧

博睿数据

可观测性 博睿数据

市场行情回暖、利好月来袭,Web3 广告业领头羊 Verasity 或迎爆发

威廉META

GitHub Universe 2023:AI 技术引领软件开发创新浪潮

SEAL安全

GitHub AI 企业号11月PK榜

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