写点什么

成功实现依赖注入

  • 2011-12-29
  • 本文字数:2350 字

    阅读完需:约 8 分钟

我在写《Dependency Injection in .NET》时经常碰到的一个反应是“你怎么把依赖注入写成一整本书?”这种难以置信的反应是很自然的,如果你觉得依赖注入的主要模式(构造函数注入)非常容易理解。

虽然这个主要模式易于理解,却很难成功实现依赖注入,因为这个机制只是一个更大的上下文里的一部分。DI 是对控制反转(IoC)原则的应用,想要成功实现IoC,你就要把你的思维逆转过来。这篇文章勾画了成功实现DI 所需的心智模型。

松散耦合:依赖注入 vs. 服务定位器

如果你不理解DI 的目的,就很容易把它实现错。这是我最近看到的一个例子:

private readonly ILog log;

public MyConsumer(ILog log)
{
this.log = log ?? LogManager.GetLogger(“My”);
}

从封装的角度来看,这个方案的主要问题是 MyConsumer 类好像无法确定它是否对日志程序这个依赖的创建拥有控制权。虽然这个示例很简单,但如果 LogManager 返回的 ILog 实例包装了一个非托管资源,需要在不使用的时候释放掉,那就有可能演变成一个问题。

这样的实现之所以会出现,是因为开发者把全部精力放在让 MyConsumer 可以进行单元测试。这样做的理由是开发者只想在单元测试的时候可以替换 ILog,其它情况应该使用 LogManager 返回的实例。

这实际上就是 Bastard Injection 反模式。其中一个潜在问题是它很容易违反里氏代换原则,因为某个特定的实现得到了特殊对待。

DI(Dependency Injection,依赖注入)的目的比起单纯地协助进行单元测试要广泛的多。它的目的是实现松散耦合,以便提升整个解决方案的可维护性。(如果你想知道为什么松散耦合能够增加可维护性,我的书的第一章讨论了这个话题,你可以免费下载试读。)

松散耦合可被概述为基于接口而不是具体实现进行编程的思想。但是,因为接口没有构造函数,如何创建那些接口的实例马上就成了一个问题。

根据你的编程方式,有两种完全不同的方案可以获取接口的实例:

对于前面的示例,很容易就会演变成服务定位器(Service Locator)反模式,像这样:

复制代码
<span color="#0000ff">public</span> MyConsumer()
{
    <span color="#0000ff">this</span>.log = <span color="#0080ff">Locator</span>.Resolve<<span color="#0080ff">ILog</span>>();
}

除了服务定位器的其它问题,这种方案的问题还在于 LogManager.GetLogger(“My”) 方法调用所需的参数丢失了。假设其它 consumer 对象需要的日志程序是通过不同的参数实例化的,那么这个版本的服务定位器就无法工作了。

这通常会导致定位器的 Resolve 方法有一个或多个重载版本,以便向服务定位器提供上下文信息。这样,离违反里氏代换原则就不远了。

DI 提供了一个更好的方案:

复制代码
<span color="#0000ff">private readonly</span> <span color="#0080ff">ILog</span> log;
<p><span color="#0000ff">public</span> MyConsumer(<span color="#0080ff">ILog</span> log)<br></br>{<br></br>    <span color="#0000ff">if</span> (log == <span color="#0000ff">null</span>)<br></br>        <span color="#0000ff">throw new</span> <span color="#0080ff">ArgumentNullException</span>(<span color="#c0504d">"log"</span>);<br></br>    <span color="#0000ff">this</span>.log = log;<br></br>}</p>

这是控制反转的纯粹形式。ILog 的任何实现都能接受,同时通过条件语句保证这个实例不为 null。这跟使用服务定位器相反,上下文没有在构建对象图的时候丢失。

复制代码
<span color="#0000ff">var</span> consumer = <span color="#0000ff">new</span> <span color="#0080ff">MyConsumer</span>(
    <span color="#0080ff">LogManager</span>.GetLogger(<span color="#c0504d">"My"</span>));

在创建 MyConsumer 的实例时,负责创建的代码知道这个特定的 consumer 对象使用哪个实现 ILog 接口的类,因此可以根据上下文提供正确的实现。

DI 和服务定位器是实现松散耦合的两种互斥方案。技术上,两种都是可行的,但 DI 没有服务定位器的缺点。

DI 的唯一缺点是它不像服务定位器那样易于理解。想要成功实现 DI,你需要克服一些障碍。

通往依赖注入的崎岖之路

学习 DI 的其中一个挑战,也是你首先碰到的最难的问题:如何获取一个接口的实例?好消息是一旦你理解构造函数注入只是简单地通过构造函数请求一个实例,你就跨越了这个最艰难的障碍。

接下来的挑战比较容易解决,往后一个更加容易。我喜欢把这些挑战想象成你需要攀越的山。第一个又高又陡,但下一个会比较容易,从那之后很快就会变得平坦:

在成功实现 DI 的路上,你的第一个障碍是理解通过构造函数注入把构建 consumer 对象及其依赖的责任委托给第三方。这个第三方就是对象组合的根(Composition Root),它是应用程序里的一个独立的点,整个对象图都是在这里构建的。因为对象组合的根负责构建整个对象图,所以它掌握了整个上下文,这使它可以对谁依赖谁这个问题做出明智的决定。

这可能吓跑了一些开发者,因为他们害怕这会导致性能问题,但事实并非如此

对于大多数人来说,第二个障碍可能是某个依赖的确定需要一个运行时的值。这种情况通常发生在某个依赖要等用户在用户界面上做出特定的选择时才能确定。对于这种情况,抽象工厂通常是一个解决方案

根据我的经验,最先的两个障碍是最难克服的。其它挑战也会出现,但一般都是个别现象。在我的书的第六章里,我收集了人们可能碰到的常见问题,以及解决它们的办法。

不管怎样,一旦你对DI 形成了正确的心智模型,任何挑战都能轻易解决的。

关于作者

Mark Seemann AutoFixture 的创作者,也是《Dependency Injection in .NET》的作者。他是一个专业的软件开发者和架构师,他住在丹麦的哥本哈根,目前是一家丹麦咨询公司Commentor 的软件架构师。他喜欢阅读、画画、弹吉他、好酒以及美食。

查看英文原文: Succeeding with Dependency Injection

2011-12-29 09:019815

评论

发布
暂无评论
发现更多内容

【活动报名】Apache ShardingSphere Dev Meetup 重启!

SphereEx

开源项目 开源社区 ShardingSphere Meetup SphereEx

恒源云(GPUSHARE)_GPU白嫖大法来袭!

恒源云

深度学习 gpu 算力加速

秒过!度目智慧通行让常态化防疫更高效

百度大脑

人工智能 人脸识别

后端开发实战总结 | 签约计划第二季|后端

阿Q说代码

内容合集 签约计划第二季 技术专题合集

全程干货,用 python 下载某站全部【免抠图片】,图片背景透明,格式PNG

梦想橡皮擦

12月日更

Android技术分享| ViewPager2离屏加载,实现抖音上下视频滑动

anyRTC开发者

android 音视频 移动开发 ViewPager 视频滑动

PackML从会到不会——标签(3)

陈的错题集

标准化 PackML

IoT Stack 2.0升级物模型及数据交互协议, 大幅提升物联网方案交付速度

百度大脑

人工智能 百度 物联网

如何在 ShardingSphere 中开发自己的 DistSQL

SphereEx

Java 数据库 ShardingSphere database SphereEx

长连接网关技术专题(六):石墨文档单机50万WebSocket长连接架构实践

JackJiang

websocket 即时通讯 IM 网关

【量化】股市技术分析利器之TA-Lib(一)

恒生LIGHT云社区

量化投资 量化

大数据中不同文件格式的比较

吴脑的键客

大数据 云存储

Linux学习方法《Linux一学就会》Linux系统进程管理

侠盗安全

Linux linux运维 运维工程师 云计算架构师

还在用BeanUtils拷贝对象?MapStruct才是王者!【附源码】

阿Q说代码

Java MapStruct 签约计划第二季 深拷贝与浅拷贝

高校企业双向赋能,首届飞桨启航菁英计划圆满结束

百度大脑

人工智能 百度 飞桨

Java中抽象类和接口的区别

编程江湖

JAVA开发 java编程

实战篇:Security+JWT组合拳 | 附源码

阿Q说代码

spring security JWT 签约计划第二季 权限验证

如果还不懂如何使用 Consumer 接口,来公司我当面给你讲!

阿Q说代码

函数式接口 签约计划第二季 consumer 实战讲解 supplier

Spark从入门到精通

冇先生

Flink CDC 系列 - 构建 MySQL 和 Postgres 上的 Streaming ETL

Apache Flink

大数据 flink 编程 后端 实时计算

语法糖甜不甜?巧用枚举实现“状态”转换限制

阿Q说代码

枚举 签约计划第二季 语法糖 订单状态转换

看了这么多年西游记,你可知道孙悟空是如何召唤土地公公的吗?

阿Q说代码

Java 观察者模式 签约计划第二季 事件通知机制

「Spark从精通到重新入门(一)」Spark 中不可不知的动态优化

尔达Erda

云计算 大数据 spark 开发者 感悟

看FusionInsight Spark如何支持JDBCServer的多实例特性

华为云开发者联盟

大数据 SQL语句 FusionInsight Spark JDBCServer 多实例

实战篇:断点续传?文件秒传?手撸大文件上传

阿Q说代码

断点续传 签约计划第二季 文件秒传 文件分块 文件合并

『上线』OpenSEC SIGs 终于成立了!

SphereEx

开源社区 ShardingSphere SphereEx 中文开源 OpenSEC

用户登录设计之双token设计

CRMEB

12.7直播预告|「淼懂·观测云实践学堂」全新栏目上线!带你走进观测云的精彩世界!

观测云

可观测性 直播 可观测

对象存储手把手教七 | 存储空间授权策略 Bucket Policy

QingStor分布式存储

分布式系统 对象存储 分布式存储 分布式,

Flink 是如何统一批流引擎的

编程江湖

大数据 flink

看了同事写的代码,我竟然开始默默的模仿了。。。

阿Q说代码

策略模式 多态 签约计划第二季 自定义参数解析器 统一验签

成功实现依赖注入_.NET_Mark Seemann_InfoQ精选文章