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

等级 346 0 0

使用 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/,如有侵权,请联系删除。

收藏
评论区

相关推荐

30分钟带你了解Web工程师必知的Docker知识
前言 笔者之前和朋友一直在讨论web技术方向的话题,也一直想了解web运维方面的知识,所以特意请教了一下我的朋友老胡,他对web运维和后端技术有非常多的实战经验,所以在本
一文搞懂Spring依赖注入
前言 提起Spring,大家肯定不陌生,它是每一个Java开发者绕不过去的坎。Spring 框架为基于 java 的企业应用程序提供了一整套解决方案
头条研发-SRE运维研发实习生视频面试(一, 二面)
(about:blank%E4%B8%80%E9%9D%A230min "一面 (30min)")一面 (30min) 江湖规矩自我介绍, 很罕见的没有用算法题起手, 直接就问很具体的问题, 点个赞 <3 (htt
使用 IoC 容器来简化业务对象的管理
使用 IoC 容器来简化业务对象的管理 有过复杂业务应用编写经验的开发人员都知道业务对象的创建是一件比较麻烦的事儿。这些应用中存在着大量的业务对象,它们之间有着复杂的依赖关系,导致模块之间很容易出现循环依赖。此外,有些对象还有单例要求,依赖之间还有顺序要求,这些更加重了问题的严重性。这种情况下就需要有一种手段来简化业务对象的管理,包括创建和获取,IoC(I
运维监控系统——Zabbix简介
前言对于运维人员来说,监控是非常重要的,因为如果想要保证线上业务整体能够稳定运行,那么我们则需要实时关注与其相关的各项指标是否正常,而一个业务系统的背后,往往存在着很多的服务器、网络设备等硬件资源,如果我们想要能够更加方便的、集中的监
运维安全-信息安全
本文转自 ,如有侵权,请联系删除。
运维,关于监控的那些事,你有必要了解一下
作者 | 乔克 来源 | 运维开发故事监控是整个运维以及产品整个生命周期最重要的一环,它旨在事前能够及时预警发现故障,事中能够结合监控数据定位问题,事后能够提供数据用于分析问题。一、监控的目的监控贯穿应用的整个生命周期。即从程序设计、开发、部署、下线。其主要的服务对象有: 技术 业务 技术通过监控系统可以了解技术的环
数据库运维做些什么?
一. 数据库生命周期 结合软件生命周期、项目的开展,数据库的生命周期大致可分为这么几个阶段。 (https://imghelloworld.osscnbeijing.aliyuncs.com/8552b8c2942bb8ce23
DevOps简介
DevOps 是一个完整的面向IT运维的工作流,以 IT 自动化以及持续集成(CI)、持续部署(CD)为基础,来优化程式开发、测试、系统运维等所有环节。DevOps的概念DevOps一词的来自于Development和Operations的组合,突出重视软件开发人员和运维人员的沟通合作,通过自动化流程来使得软件构建、测试、发布更加快捷、频繁和可靠。
DevOps概述
DevOps概述DevOps(Development和Operations的组合词)是一组过程、方法与系统的统称,用于促进开发(应用程序/软件工程)、技术运营(运维)、质量保障(测试)(QA)部门之间的沟通、协作与整合。随着业务复杂化和人员的增加,开发人员和运维人员逐渐演化成两个独立的部门,他们工作地点分离,工具链不同,业务目标也有差异,这使
DevOps与CICD的区别 及 docker、k8s的CICD思路
1\. DevOps简介DevOps 就是开发(Development)、测试(QA)、运维(Operations)这三个领域的合并。image.png为什么要合并这三个领域?主要是开发和运维的脱节。DevOps是一种思想、一组最佳实践、以及一种文化。DevOps落地实施,从组织架构、设计人员、流程、人员分工、人员技能到工具,变化
一份DevOps工程师职责清单,待你查阅
如果一个组织的开发人员和运维人员是独立工作的模式,实施DevOps就需要对组织进行大的调整。因为,只有具备合适的组织人员,文化和工具来才能成功实施DevOps。根据显示,实施DevOps的最常见的障碍之一是员工缺乏技能。什么是DevOps工程师?DevOps工程师是一位IT专家,应该对开发和运维工作都有广泛的了解,包括编码,基础
运维大佬嘲笑我,这个你都不知道?
大家好,我是阿沐,一个喜欢分享技术而且爱好写散文的程序员。今天来给大家介绍一下info命令查看redis具体的详细信息讲解!起因是:前几年我在老家郑州实习面试(那个时候还没有毕业)的时候遇到面试官提问;面试官来于百度总部的工程师6年java开发经验+3年多的PHP开发经验,我在他的面前基本就是弟弟中的弟弟,虽然勉强通过入职了,但是却被运维无情地嘲笑,就因为组
自己动手打造一套IOC注解框架
1.概述 这是我们的内涵段子系统架构的第一期分享,。在介绍内涵段子整个项目的时候我们也说好了会分析系统源码设计模式,第三方框架源码解析,然后自己动手一点一点打造一套内涵段子框架。这一期的内容对于部分哥们可能有点麻烦,如果觉得抽象请看视频讲解。   那么什么是IOC,控制反转(Inversion of Control,英文缩写为IOC),其实就是反射加注解
Android Studio插件开发之 - IOC注解生成器
1.概述 上一期我们已经分享了。那么现在我们来动手写一个IOC注解生成器,有点类似于ButterKnife的插件一样自动给我们生成代码,在网上找了很多资料国内基本就在HelloWorld阶段,也有很多哥们向我反应插件的代码还是有点蒙B。代码方面能理解就理解,不理解也不强求,如果你能改一改别人已经写好的插件就最好了,实在不行我们干脆也别折腾了大不了不用,本文