反模式 DI anti-patterns
反模式 DI anti-patterns
《Dependency Injecttion Prinsciples,Practices, and Patterns》—— StevenVan Deursen and Mark Seemann
一、一、反模式 DI anti-patterns
1. 控制狂 Control freak
在程序设计中,"Control freak"(控制狂)通常指的是一种反模式,即过度控制和过度管理代码的设计和执行流程。这种情况下,程序员试图通过过度的控制和指令来达到对代码的绝对控制,而忽视了灵活性、可扩展性和可维护性。
控制狂在程序设计中可能表现为以下行为:
过度复杂的控制逻辑:设计过于复杂的控制逻辑,使得代码难以理解和维护。
过度使用全局状态:滥用全局变量或全局状态,使得代码之间的依赖关系变得混乱和难以追踪。
过度依赖条件语句:过多地使用条件语句(如if-else语句),导致代码逻辑分散、冗长和难以扩展。
过度使用硬编码:将具体数值、路径或配置直接硬编码到代码中,而不是使用配置文件或参数来实现灵活性。
这些行为会导致代码的可读性、可维护性和可扩展性下降,增加了代码的复杂性和脆弱性。相反,良好的程序设计应该追求简洁、模块化和松耦合的原则,以实现可维护、可扩展和易于理解的代码。
1-1. 示例:
- ...
- private readonly IProductRepository _productRepository;
- public ProductService()
- {
- //反模式范例,直接new一个SqlProductRepository,导致紧耦合
- _productRepository = new SqlProductRepository();
- }
- ...
1-2. 工厂模式下的反模式
实体工厂可以解决一些复杂物件建立逻辑封装在工厂中,可以避免写重复的代码。 但是在DI架构下,工厂模式没有带来益处。
- public class ProductRepositoryFactory
- {
- public static IProductRepository CreateProductRepository()
- {
- //工厂模式反模式范例,程序变复杂,只是控制狂换了个位置
- return new SqlProductRepository();
- }
- }
即便将上述代码改为读取外部配置的静态工厂,仍然是对问题换了个地方。ProductServiece依赖ProductRepositoryFactory,ProductRepositoryFactory又依赖于SqlProductRepository和AzureProductRepository,因此依赖传递导致ProductServiece对后两者的依赖。
- public static IProductRepository Create()
- {
- IConfigurationRoot configuration = new ConfigurationBuilder()
- .SetBasePath(Directory.GetCurrentDirectory())
- .AddJsonFile("appsettings.json")
- .Build();
- string repositoryType = configuration["productRepository"];
- switch (repositoryType)
- {
- case "sql": return new SqlProductRepository();
- case "azure": return AzureProductRepository();
- default: throw new InvalidOperationException("...");
- }
- }

1-3. 构造重载时的控制狂
下面案例中无参构造将SqlProductRepository作为外部预设,造成业务层对SQL数据层的依赖耦合
- private readonly IProductRepository repository;
- public ProductService()
- : this(new SqlProductRepository())
- {
- }
- public ProductService(IProductRepository repository)
- {
- if (repository == null)
- throw new ArgumentNullException("repository");
- this.repository = repository;
- }
2. 服务定位
服务定位是指在组合根之外的位置,一步特定的一群不稳定依赖对象,作为依赖需求组件提供给应用程序。
在程序设计中,
组合根”是指应用程序的起始点,负责组合(compose)整个应用程序的各个部分(如依赖注入容器、对象实例化、配置设置等)。Composition root通常位于应用程序的最顶层,是整个应用程序的组装中心。
具体来说,“组合根”负责以下几个主要任务:
- 实例化和配置应用程序中的各种对象和组件。
- 注册依赖关系,进行依赖注入(Dependency Injection)。
- 加载配置设置,设置应用程序的参数和行为。
- 协调应用程序中各个部分之间的交互和依赖关系。
通过将这些逻辑集中在一个地方,即“composition root”,可以实现应用程序的解耦和灵活性,使得应用程序的各个部分可以更容易地被替换、修改或扩展。这种设计模式有助于提高代码的可维护性和可测试性。
2-1. 示例:
- ...
- public class ProductService : IProductService
- {
- private readonly IProductRepository repository;
- //无参构造函数,让人看不清楚依赖关系
- public ProductService()
- {
- //通过服务定位组件Locator来获取实例
- this.repository = Locator.GetService<IProductRepository>();
- }
- public IEnumerable<DiscountedProduct> GetFeaturedProducts() { ... }
- }
- ...
- //简单服务定位的实现
- public static class Locator
- {
- private static Dictionary<Type, object> services =
- new Dictionary<Type, object>();
- //注册实例
- public static void Register<T>(T service)
- {
- services[typeof(T)] = service;
- }
- //获取实例
- public static T GetService<T>()
- {
- return (T)services[typeof(T)];
- }
- public static void Reset()
- {
- services.Clear();
- }
- }
- ...
- //以服务定位来进行单元测试
- [Fact]
- public void GetFeaturedProductsWillReturnInstance()
- {
- // Arrange
- var stub = ProductRepositoryStub();
- Locator.Reset();
- Locator.Register<IProductRepository>(stub);
- var sut = new ProductService();
- // Act
- var result = sut.GetFeaturedProducts();
- // Assert
- Assert.NotNull(result);
- }
2-2. 对服务定位反模式的反思
2-2-1. 服务定位的优点:
- 可以通过更改注册来支持延迟绑定。
- 可以并行开发代码,因为您是对接口进行编程,可以随意替换模块。
- 可以很好地分离关注点,因此没有什么能阻止您编写可维护的代码,但这样做会变得更加困难。
- 可以用TestDoubles来替换依赖项,从而确保了可测试性。
在程序开发中,Test Doubles是一种用于测试的替代品或模拟对象。它们被用来替代真实的依赖项或组件,以便在测试过程中隔离被测代码的行为。Test Doubles有多种类型,包括:
- Dummy Objects(哑对象):它们只是占位符,没有实际的实现,仅用于满足方法签名或参数要求。
- Fake Objects(伪对象):它们是真实的实现,但是在测试环境中使用简化的版本。例如,使用内存数据库替代真实的数据库。
- Stub Objects(存根对象):它们提供了预定义的响应,以便在测试中模拟特定的行为。例如,返回固定的数据或执行预定的操作。
- Spy Objects(间谍对象):它们类似于存根对象,但还会记录被调用的方法和参数,以便在测试中进行断言和验证。
- Mock Objects(模拟对象):它们是预先配置的对象,具有预期的行为和交互。通过使用断言来验证它们与被测代码之间的交互是否符合预期。
使用Test Doubles可以帮助在测试过程中隔离和控制依赖项,使测试更加可靠、可重复和可维护。这些替代品可以根据测试需要进行创建和配置,以模拟各种场景和条件。
2-2-2. 服务定位的坏处:
和服务定位器绑定的一些类别可能成为冗余
这个类使它的依赖性变得不明显

注意
在组合根区域使用DI容器,并不算服务定位反模式,它是一个基础框架组件。
3. 环境上下文
ambient context即环境上下文。在软件开发中,ambient context 是指一种模式,用于在应用程序中共享环境相关的信息或配置,而无需显式传递这些信息给每个组件或方法。这种模式通常通过线程本地存储(Thread Local Storage)或者类似的机制实现。一般在组合根之外的地方,透过一个全局存取的static修饰子类别成员。
通过 ambient context 模式,可以在整个应用程序中访问共享的环境信息,比如当前用户身份、语言设置、主题样式等,而无需在每个方法或组件中显式地传递这些信息。这种模式的优点在于可以简化代码,提高可维护性,并且避免了在每个组件中传递相同的上下文信息的重复工作。
需要注意的是,虽然 ambient context 可以简化代码,但过度使用它可能会导致代码难以理解和调试,因为它引入了隐式的依赖关系。因此,在使用 ambient context 模式时需要权衡利弊,并遵循良好的设计原则。
3-1. 示例
- public string GetWelcomeMessage()
- {
- ITimeProvider provider = TimeProvider.Current;
- DateTime now = provider.Now;
- string partOfDay = now.Hour < 6 ? "night" : "day";
- return string.Format("Good {0}.", partOfDay);
- }
3-2. 示例-查询时间用的环境上下文
- //查询当前系统时间的管道
- public interface ITimeProvider
- {
- DateTime Now { get; }
- }
- ...
- 静态类提供全局范围可读取实例
- public static class TimeProvider
- {
- //内建预设初始化
- private static ITimeProvider current =
- new DefaultTimeProvider();
- //全局范围类可对ITimeProvider不稳定依赖进行读取,设置的静态属性成员
- public static ITimeProvider Current
- {
- get { return current; }
- set { current = value; }
- }
- //预设
- private class DefaultTimeProvider : ITimeProvider
- {
- public DateTime Now { get { return DateTime.Now; } }
- }
- }
以环境物件反模式进行单元测试
- [Fact]
- public void SaysGoodDayDuringDayTime()
- {
- // Arrange
- DateTime dayTime = DateTime.Parse("20190101 6:00");
- var stub = new TimeProviderStub { Now = dayTime };
- //将原本预设替换为测试用替身
- TimeProvider.Current = stub;
- //WelcomeMessageGenerator构造API未揭露其需要ITimeProvier这层关系
- var sut = new WelcomeMessageGenerator();
- // Act
- string actualMessage = sut.GetWelcomeMessage(); //TimeProvider.Current与GetWelcomeMessage时序耦合
- // Assert
- Assert.Equal(expected: "Good day.", actual: actualMessage);
- }
3-3. 对环境上下文反模式反思
3-3-1. 环境上下文弊端
- “依赖项”已被隐藏起来了。
- 测试变得更加困难。
- 很难根据其上下文来改变依赖关系。
- 在依赖的初始化和使用之间存在时间耦合。
3-3-2. 将环境上下文重回DI正途
- 将这些环境上下文的调用集中到一处,这个订房的绝佳选择就是调用构造器的时候。
- 建立一个private readonly私有只读成员,用于存放透过环境上下文索取的依赖对象,之后类别中的所有需要这份以来的程序,都直接引用这个新的私有成员。
改进后的程序:
- public class WelcomeMessageGenerator
- {
- private readonly ITimeProvider timeProvider;
- public WelcomeMessageGenerator(ITimeProvider timeProvider)
- {
- if (timeProvider == null)
- throw new ArgumentNullException("timeProvider");
- this.timeProvider = timeProvider;
- }
- public string GetWelcomeMessage()
- {
- DateTime now = this.timeProvider.Now;
- ...
- }
- }
4. 限制性构造
限制性构造指某个抽象接口在实例化时,强制需要这些实体类别中有个同样识别定义的构造器,一遍延迟绑定。
4-1. 示例
强制对构造函数执行精确的签名
- public class SqlProductRepository : IProductRepository
- {
- public SqlProductRepository(string connectionStr)
- {
- }
- }
- public class AzureProductRepository : IProductRepository
- {
- public AzureProductRepository(string connectionStr)
- {
- }
- }
4-2. 示例-ProductRepository的延迟绑定
- string connectionString = this.Configuration
- .GetConnectionString("CommerceConnectionString");
- var settings =
- this.Configuration.GetSection("AppSettings");
- string productRepositoryTypeName =
- settings.GetValue<string>("ProductRepositoryType");
- var productRepositoryType =
- Type.GetType(
- typeName: productRepositoryTypeName,
- throwOnError: true);
- var constructorArguments =
- new object[] { connectionString };
- IProductRepository repository =
- (IProductRepository)Activator.CreateInstance(
- productRepositoryType, constructorArguments);
- ...
- {
- "ConnectionStrings": {
- "CommerceConnectionString":
- "Server=.;Database=MaryCommerce;Trusted_Connection=True;"
- },
- "AppSettings": {
- "ProductRepositoryType": "SqlProductRepository, Commerce.SqlDataAccess"
- },
- }
实际上,这没有意义,因为这表示了对依赖项的构造函数的意外约束。在这种情况下,您有一个隐式的要求,即IProductRepository的任何实现都应该有一个以单个字符串作为输入的构造函数,这就使IProductRepository实例化时受到额外的限制。
4-3. 限制性构造反模式的反思
虽然类似上面代码中这种限制最普遍,但在灵活性方面的成本是巨大的。无论您如何约束对象构造,您都会失去灵活性。
当您有多个类需要相同的依赖关系时,您可能希望在所有这些类之间共享一个实例。只有当您可以从外部注入该实例时,这才有可能实现。尽管您可以在每个类中编写代码,以从配置文件中读取类型信息并使用Activator.CreateInstance。 CreateInstance来创建正确的实例类型,它确实需要以这种方式共享单个实例。相反,同一类的多个实例会占用更多的内存。
4-4. 将限制器重回正途
借助抽象工厂模式,来产生抽象接口的实例,然后通过抽象接口的类别来配合某个构造器识别定义。

- public class SqlProductRepository : IProductRepository
- {
- private readonly IUserContext userContext;
- private readonly CommerceContext dbContext;
- public SqlProductRepository(
- IUserContext userContext, CommerceContext dbContext)
- {
- if (userContext == null)
- throw new ArgumentNullException("userContext");
- if (dbContext == null)
- throw new ArgumentNullException("dbContext");
- this.userContext = userContext;
- this.dbContext = dbContext;
- }
- }
SqlProductRepository 实现了IproductRespository接口,并且没有对IUserContext对象产生依赖。
负面案例
- public class SqlProductRepositoryFactory
- : IProductRepositoryFactory
- {
- private readonly string connectionString;
- //传入Mircorsoft的IConfigurationRoot让后读取需要的设定值。玩意要是设定值不存在,会在构建时抛出异常
- public SqlProductRepositoryFactory(
- IConfigurationRoot configuration)
- {
- this.connectionString =
- configuration.GetConnectionString(
- "CommerceConnectionString");
- }
- public IProductRepository Create()
- {
- //使用位于不同程序集中的依赖项创建一个新的IProductRepositoryFactory
- return new SqlProductRepository(
- new AspNetUserContextAdapter(),
- new CommerceContext(this.connectionString));
- }
- }
反模式 DI anti-patterns的更多相关文章
- Python编程中的反模式
Python是时下最热门的编程语言之一了.简洁而富有表达力的语法,两三行代码往往就能解决十来行C代码才能解决的问题:丰富的标准库和第三方库,大大节约了开发时间,使它成为那些对性能没有严苛要求的开发任务 ...
- 重构24-Remove Arrowhead Antipattern(去掉箭头反模式)
基于c2的wiki条目.Los Techies的Chris Missal同样也些了一篇关于反模式的post. 简单地说,当你使用大量的嵌套条件判断时,形成了箭头型的代码,这就是箭头反模式(arrow ...
- ORM 是一种讨厌的反模式
本文由码农网 – 孙腾浩原创翻译,转载请看清文末的转载要求,欢迎参与我们的付费投稿计划! (“Too Long; Didn’t Read.”太长不想看,可以看这段摘要 )ORM是一种讨厌的反模式,违背 ...
- Apache Hadoop最佳实践和反模式
摘要:本文介绍了在Apache Hadoop上运行应用程序的最佳实践,实际上,我们引入了网格模式(Grid Pattern)的概念,它和设计模式类似,它代表运行在网格(Grid)上的应用程序的可复用解 ...
- 开发反模式 - SQL注入
一.目标:编写SQL动态查询 SQL常常和程序代码一起使用.我们通常所说的SQL动态查询,是指将程序中的变量和基本SQL语句拼接成一个完整的查询语句. string sql = SELECT * FR ...
- 开发反模式(GUID) - 伪键洁癖
一.目标:整理数据 有的人有强迫症,他们会为一系列数据的断档而抓狂. 一方面,Id为3这一行确实发生过一些事情,为什么这个查询不返回Id为3的这一行?这条记录数据丢失了吗?那个Column到底是什么? ...
- 查询反模式 - 正视NULL值
一.提出问题 不可避免地,我们都数据库总有一些字段是没有值的.不管是插入一个不完整的行,还是有些列可以合法地拥有一些无效值.SQL 支持一个特殊的空值,就是NULL. 在很多时候,NULL值导致我们的 ...
- SQL反模式学习笔记1 开篇
什么是“反模式” 反模式是一种试图解决问题的方法,但通常会同时引发别的问题. 反模式分类 (1)逻辑数据库设计反模式 在开始编码之前,需要决定数据库中存储什么信息以及最佳的数据组织方式和内在关联方式. ...
- SQL反模式学习笔记5 外键约束【不用钥匙的入口】
目标:简化数据库架构 一些开发人员不推荐使用引用完整性约束,可能不使用外键的原因有一下几点: 1.数据更新有可能和约束冲突: 2.当前的数据库设计如此灵活,以至于不支持引用完整性约束: 3.数据库为外 ...
- SQL反模式学习笔记3 单纯的树
2014-10-11 在树形结构中,实例被称为节点.每个节点都有多个子节点与一个父节点. 最上层的节点叫做根(root)节点,它没有父节点. 最底层的没有子节点的节点叫做叶(leaf). 中间的节点简 ...
随机推荐
- WPF 对接 Vortice 绘制 WIC 图片
本文告诉大家如何通过 Vortice 在 Direct2D 里面绘制图片,图片的来源是 WIC 加载出的图片 在上一篇博客 WPF 对接 Vortice 调用 WIC 加载图片 告诉了大家如何对接 V ...
- FPGA最大工作频率教程
FPGA最大工作频率教程 1. Quartus的时序分析 作为编译过程的一部分,Quartus对布局布线的电路做时序分析.在编译报告里,展开"Timing Analyzer",这 ...
- ThreadLocal 的原理讲述 + 基于ThreadLocal实现MVC中的M层的事务控制
ThreadLocal 的原理讲述 + 基于ThreadLocal实现MVC中的M层的事务控制 目录 ThreadLocal 的原理讲述 + 基于ThreadLocal实现MVC中的M层的事务控制 每 ...
- 【GUI软件】小红书详情数据批量采集,含笔记内容、转评赞藏等,支持多笔记同时采集!
目录 一.背景介绍 1.1 爬取目标 1.2 演示视频 1.3 软件说明 二.代码讲解 2.1 爬虫采集模块 2.2 软件界面模块 2.3 日志模块 三.获取源码及软件 一.背景介绍 1.1 爬取目标 ...
- Jmeter-线程组下篇
线程组 线程组作为JMeter测试计划的核心组件之一,对于模拟并发用户的行为至关重要.线程组元件是整个测试计划的入口,所有的取样器和控制器必须放置在线程组下. 可以将线程组视为一个虚拟用户池,其中每个 ...
- 在 ThinkPad E470 上安装 Ubuntu 16.04 无线网卡驱动
目录 文章目录 目录 安装 安装 # 查看无线网卡驱动类型,E470 一般为 RTL8821CE lspci # 安装必要工具 sudo apt-get install build-essential ...
- C# 借助NPOI 完成 xls 转换为xlsx
背景:MinExcel开源类库,导数据的库,占用内存很低,通过io,不通过内存保存,不支持 xls格式的文件,支持csv和xlsx,所以要想使用这个库,就得把xls格式转换为xlsx.只复制了数据 合 ...
- synchronized锁升级过程
更过博文请关注:https://blog.bigcoder.cn JDK 1.6后锁的状态总共有四种,级别由低到高依次为:无锁.偏向锁.轻量级锁.重量级锁,这四种锁状态分别代表什么,为什么会有锁升级? ...
- 记录一下Android usb相关的知识学习
在SecondStageMain中会先调用PropertyInit做属性初始化,该方法会调用PropertyLoadBootDefaults加载持久化的属性主要加载位置: /system/build. ...
- SwiftUI Stack中的View被压缩的效果
一.背景 我们在布局中,经常会遇到视图元素排列时空间不足或者空间过大的情况,在这种场景下面,不同的布局方式有不同的方法: 绝对布局frame:纯靠计算过程控制,获取父视图的大小,根据需求,计算自己需要 ...
注意