写点什么

成功实现依赖注入

  • 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:019225

评论

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

自动化测试框架类型,你知道几种?此处介绍5种比较常见的

软测小生

软件测试 自动化测试框架 软件自动化测试

标准的开发框架,对企业开发有多重要?

Learun

敏捷开发 快速开发

架构师训练营第五周学习总结

尹斌

批处理 有状态等应用类型在K8S上应该如何配置?

东风微鸣

Kubernetes 最佳实践

学了那么多 NoSQL 数据库 NoSQL 究竟是啥

哈喽沃德先生

数据库 nosql 非关系型数据库

mPaaS x Menxlab | 1024程序员节:Talk is cheap,Show me the AppID

蚂蚁集团移动开发平台 mPaaS

程序员 开发者 mPaaS 1024

LeetCode题解:98. 验证二叉搜索树,递归中序遍历过程中判断,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

JavaScript 类型 — 重学 JavaScript

三钻

Java 大前端

机器学习是什么?

马同学

学习

全面到哭!BAT内部Java求职面试宝典,必须人手一份!

Java架构之路

Java 程序员 架构 面试 编程语言

ArCall功能介绍手册

anyRTC开发者

ios 音视频 WebRTC RTC 安卓

趣味科普丨一文读懂云服务器的那些事儿

华为云开发者联盟

镜像 服务器 服务

GitLab用户切换引发的某程序员“暴动”,怒而开源项目源码

小Q

Java git 学习 开发 代码仓库

DDIA 读书笔记(2)数据模型的存储与检索

莫黎

读书笔记

学习总结

饺子

spring-boot-route(二十二)实现邮件发送功能

Java旅途

Java Spring Boot 发送邮件

typora增强-mac

老菜鸟

Typora

AI让远程交流“更清晰”:GAN消除视频通话中的抖动

Microsoft Azure机器学习采用NVIDIA AI为Word编辑器提供语法建议

千万不要往 Shell 里粘贴命令!

大道至简

命令行

容器化应用系统上生产的最佳实践

东风微鸣

Kubernetes 最佳实践 生产

攻克金融系统开发难点,借助SpreadJS实现在线导入Excel自定义报表

葡萄城技术团队

SpreadJS 在线导入excel

iOS性能优化 — 一、crash监控及防崩溃处理

iOSer

性能优化 ios开发 Crash 监控及防崩溃处理

数据湖探索DLI新功能:基于openLooKeng的交互式分析

华为云开发者联盟

数据 处理

架构师训练营第一周课后作业

李日盛

吃透阿里大佬整理的Java面试要点手册,成功五面进阿里(二本学历)

Java架构追梦

Java 学习 架构 面试 核心知识点整理

面试官的灵魂一击:你懂 MySQL 事务日志吗?

Java架构师迁哥

vivo 商城前端架构升级—前后端分离篇

vivo互联网技术

Java 大前端 前后端分离

解析 CloudQuery 审计分析功能

BinTools图尔兹

数据库 sql 安全 工具软件

架构训练营第一周学习小结

李日盛

1分钟带你get React setState 面试要点

Leo

面试 大前端 React setState

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