反模式 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"(控制狂)通常指的是一种反模式,即过度控制和过度管理代码的设计和执行流程。这种情况下,程序员试图通过过度的控制和指令来达到对代码的绝对控制,而忽视了灵活性、可扩展性和可维护性。

控制狂在程序设计中可能表现为以下行为:

  1. 过度复杂的控制逻辑:设计过于复杂的控制逻辑,使得代码难以理解和维护。

  2. 过度使用全局状态:滥用全局变量或全局状态,使得代码之间的依赖关系变得混乱和难以追踪。

  3. 过度依赖条件语句:过多地使用条件语句(如if-else语句),导致代码逻辑分散、冗长和难以扩展。

  4. 过度使用硬编码:将具体数值、路径或配置直接硬编码到代码中,而不是使用配置文件或参数来实现灵活性。

这些行为会导致代码的可读性、可维护性和可扩展性下降,增加了代码的复杂性和脆弱性。相反,良好的程序设计应该追求简洁、模块化和松耦合的原则,以实现可维护、可扩展和易于理解的代码。

1-1. 示例:

  1. ... 


  2. private readonly IProductRepository _productRepository; 



  3. public ProductService() 





  4. //反模式范例,直接new一个SqlProductRepository,导致紧耦合 


  5. _productRepository = new SqlProductRepository(); 





  6. ... 


1-2. 工厂模式下的反模式

实体工厂可以解决一些复杂物件建立逻辑封装在工厂中,可以避免写重复的代码。 但是在DI架构下,工厂模式没有带来益处。

  1. public class ProductRepositoryFactory 





  2. public static IProductRepository CreateProductRepository() 





  3. //工厂模式反模式范例,程序变复杂,只是控制狂换了个位置 


  4. return new SqlProductRepository(); 








即便将上述代码改为读取外部配置的静态工厂,仍然是对问题换了个地方。ProductServiece依赖ProductRepositoryFactoryProductRepositoryFactory又依赖于SqlProductRepositoryAzureProductRepository,因此依赖传递导致ProductServiece对后两者的依赖。

  1. public static IProductRepository Create() 





  2. IConfigurationRoot configuration = new ConfigurationBuilder() 


  3. .SetBasePath(Directory.GetCurrentDirectory()) 


  4. .AddJsonFile("appsettings.json") 


  5. .Build(); 


  6. string repositoryType = configuration["productRepository"]; 


  7. switch (repositoryType) 





  8. case "sql": return new SqlProductRepository(); 


  9. case "azure": return AzureProductRepository(); 


  10. default: throw new InvalidOperationException("..."); 









IProductRepository

1-3. 构造重载时的控制狂

下面案例中无参构造将SqlProductRepository作为外部预设,造成业务层对SQL数据层的依赖耦合

  1. private readonly IProductRepository repository; 


  2. public ProductService()  


  3. : this(new SqlProductRepository())  


  4. {  


  5. }  


  6. public ProductService(IProductRepository repository)  


  7. {  


  8. if (repository == null)  


  9. throw new ArgumentNullException("repository");  



  10. this.repository = repository;  





2. 服务定位

服务定位是指在组合根之外的位置,一步特定的一群不稳定依赖对象,作为依赖需求组件提供给应用程序。

在程序设计中,组合根”是指应用程序的起始点,负责组合(compose)整个应用程序的各个部分(如依赖注入容器、对象实例化、配置设置等)。Composition root通常位于应用程序的最顶层,是整个应用程序的组装中心。

具体来说,“组合根”负责以下几个主要任务:

  1. 实例化和配置应用程序中的各种对象和组件。
  2. 注册依赖关系,进行依赖注入(Dependency Injection)。
  3. 加载配置设置,设置应用程序的参数和行为。
  4. 协调应用程序中各个部分之间的交互和依赖关系。

    通过将这些逻辑集中在一个地方,即“composition root”,可以实现应用程序的解耦和灵活性,使得应用程序的各个部分可以更容易地被替换、修改或扩展。这种设计模式有助于提高代码的可维护性和可测试性。

2-1. 示例:

  1. ... 


  2. public class ProductService : IProductService 





  3. private readonly IProductRepository repository; 


  4. //无参构造函数,让人看不清楚依赖关系 


  5. public ProductService() 





  6. //通过服务定位组件Locator来获取实例 


  7. this.repository = Locator.GetService<IProductRepository>(); 





  8. public IEnumerable<DiscountedProduct> GetFeaturedProducts() { ... } 





  9. ... 


  10. //简单服务定位的实现 


  11. public static class Locator 





  12. private static Dictionary<Type, object> services =  


  13. new Dictionary<Type, object>(); 


  14. //注册实例 


  15. public static void Register<T>(T service) 





  16. services[typeof(T)] = service; 





  17. //获取实例 


  18. public static T GetService<T>()  


  19. {  


  20. return (T)services[typeof(T)];  





  21. public static void Reset() 





  22. services.Clear(); 








  23. ... 


  24. //以服务定位来进行单元测试 


  25. [Fact] 


  26. public void GetFeaturedProductsWillReturnInstance() 





  27. // Arrange 


  28. var stub = ProductRepositoryStub();  


  29. Locator.Reset();  


  30. Locator.Register<IProductRepository>(stub);  


  31. var sut = new ProductService(); 


  32. // Act 


  33. var result = sut.GetFeaturedProducts();  


  34. // Assert 


  35. Assert.NotNull(result); 





2-2. 对服务定位反模式的反思

2-2-1. 服务定位的优点

  • 可以通过更改注册来支持延迟绑定。
  • 可以并行开发代码,因为您是对接口进行编程,可以随意替换模块。
  • 可以很好地分离关注点,因此没有什么能阻止您编写可维护的代码,但这样做会变得更加困难。
  • 可以用TestDoubles来替换依赖项,从而确保了可测试性。

在程序开发中,Test Doubles是一种用于测试的替代品或模拟对象。它们被用来替代真实的依赖项或组件,以便在测试过程中隔离被测代码的行为。Test Doubles有多种类型,包括:

  1. Dummy Objects(哑对象):它们只是占位符,没有实际的实现,仅用于满足方法签名或参数要求。
  2. Fake Objects(伪对象):它们是真实的实现,但是在测试环境中使用简化的版本。例如,使用内存数据库替代真实的数据库。
  3. Stub Objects(存根对象):它们提供了预定义的响应,以便在测试中模拟特定的行为。例如,返回固定的数据或执行预定的操作。
  4. Spy Objects(间谍对象):它们类似于存根对象,但还会记录被调用的方法和参数,以便在测试中进行断言和验证。
  5. 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. 示例

  1. public string GetWelcomeMessage() 





  2. ITimeProvider provider = TimeProvider.Current;  


  3. DateTime now = provider.Now; 


  4. string partOfDay = now.Hour < 6 ? "night" : "day"; 


  5. return string.Format("Good {0}.", partOfDay); 





3-2. 示例-查询时间用的环境上下文

  1. //查询当前系统时间的管道 


  2. public interface ITimeProvider 





  3. DateTime Now { get; }  





  4. ... 


  5. 静态类提供全局范围可读取实例 


  6. public static class TimeProvider  





  7. //内建预设初始化 


  8. private static ITimeProvider current = 


  9. new DefaultTimeProvider(); 


  10. //全局范围类可对ITimeProvider不稳定依赖进行读取,设置的静态属性成员  


  11. public static ITimeProvider Current  





  12. get { return current; } 


  13. set { current = value; } 





  14. //预设 


  15. private class DefaultTimeProvider : ITimeProvider  





  16. public DateTime Now { get { return DateTime.Now; } } 








以环境物件反模式进行单元测试

  1. [Fact] 


  2. public void SaysGoodDayDuringDayTime() 





  3. // Arrange 


  4. DateTime dayTime = DateTime.Parse("2019­01­01 6:00"); 


  5. var stub = new TimeProviderStub { Now = dayTime }; 


  6. //将原本预设替换为测试用替身 


  7. TimeProvider.Current = stub; 


  8. //WelcomeMessageGenerator构造API未揭露其需要ITimeProvier这层关系 


  9. var sut = new WelcomeMessageGenerator();  


  10. // Act 


  11. string actualMessage = sut.GetWelcomeMessage(); //TimeProvider.Current与GetWelcomeMessage时序耦合 


  12. // Assert 


  13. Assert.Equal(expected: "Good day.", actual: actualMessage); 





3-3. 对环境上下文反模式反思

3-3-1. 环境上下文弊端

  • “依赖项”已被隐藏起来了。
  • 测试变得更加困难。
  • 很难根据其上下文来改变依赖关系。
  • 在依赖的初始化和使用之间存在时间耦合。

3-3-2. 将环境上下文重回DI正途

  1. 将这些环境上下文的调用集中到一处,这个订房的绝佳选择就是调用构造器的时候。
  2. 建立一个private readonly私有只读成员,用于存放透过环境上下文索取的依赖对象,之后类别中的所有需要这份以来的程序,都直接引用这个新的私有成员。

    改进后的程序:
  1. public class WelcomeMessageGenerator 





  2. private readonly ITimeProvider timeProvider; 


  3. public WelcomeMessageGenerator(ITimeProvider timeProvider) 





  4. if (timeProvider == null) 


  5. throw new ArgumentNullException("timeProvider"); 


  6. this.timeProvider = timeProvider; 





  7. public string GetWelcomeMessage() 





  8. DateTime now = this.timeProvider.Now; 


  9. ... 








4. 限制性构造

限制性构造指某个抽象接口在实例化时,强制需要这些实体类别中有个同样识别定义的构造器,一遍延迟绑定。

4-1. 示例

强制对构造函数执行精确的签名

  1. public class SqlProductRepository : IProductRepository 





  2. public SqlProductRepository(string connectionStr)  











  3. public class AzureProductRepository : IProductRepository 





  4. public AzureProductRepository(string connectionStr) 











4-2. 示例-ProductRepository的延迟绑定

  1. string connectionString = this.Configuration  


  2. .GetConnectionString("CommerceConnectionString");  


  3. var settings =  


  4. this.Configuration.GetSection("AppSettings");  



  5. string productRepositoryTypeName =  


  6. settings.GetValue<string>("ProductRepositoryType"); 


  7. var productRepositoryType =  


  8. Type.GetType(  


  9. typeName: productRepositoryTypeName,  


  10. throwOnError: true);  


  11. var constructorArguments = 


  12. new object[] { connectionString }; 


  13. IProductRepository repository =  


  14. (IProductRepository)Activator.CreateInstance(  


  15. productRepositoryType, constructorArguments); 



  16. ... 





  17. "ConnectionStrings": { 


  18. "CommerceConnectionString": 


  19. "Server=.;Database=MaryCommerce;Trusted_Connection=True;" 


  20. }, 


  21. "AppSettings": { 


  22. "ProductRepositoryType": "SqlProductRepository, Commerce.SqlDataAccess" 


  23. }, 





实际上,这没有意义,因为这表示了对依赖项的构造函数的意外约束。在这种情况下,您有一个隐式的要求,即IProductRepository的任何实现都应该有一个以单个字符串作为输入的构造函数,这就使IProductRepository实例化时受到额外的限制。

4-3. 限制性构造反模式的反思

虽然类似上面代码中这种限制最普遍,但在灵活性方面的成本是巨大的。无论您如何约束对象构造,您都会失去灵活性。

当您有多个类需要相同的依赖关系时,您可能希望在所有这些类之间共享一个实例。只有当您可以从外部注入该实例时,这才有可能实现。尽管您可以在每个类中编写代码,以从配置文件中读取类型信息并使用Activator.CreateInstance。 CreateInstance来创建正确的实例类型,它确实需要以这种方式共享单个实例。相反,同一类的多个实例会占用更多的内存。

4-4. 将限制器重回正途

借助抽象工厂模式,来产生抽象接口的实例,然后通过抽象接口的类别来配合某个构造器识别定义。


ABFac
  1. public class SqlProductRepository : IProductRepository 





  2. private readonly IUserContext userContext; 


  3. private readonly CommerceContext dbContext; 


  4. public SqlProductRepository( 


  5. IUserContext userContext, CommerceContext dbContext) 





  6. if (userContext == null) 


  7. throw new ArgumentNullException("userContext"); 


  8. if (dbContext == null) 


  9. throw new ArgumentNullException("dbContext"); 


  10. this.userContext = userContext; 


  11. this.dbContext = dbContext; 








SqlProductRepository 实现了IproductRespository接口,并且没有对IUserContext对象产生依赖。

负面案例

  1. public class SqlProductRepositoryFactory 


  2. : IProductRepositoryFactory 





  3. private readonly string connectionString; 



  4. //传入Mircorsoft的IConfigurationRoot让后读取需要的设定值。玩意要是设定值不存在,会在构建时抛出异常 


  5. public SqlProductRepositoryFactory( 


  6. IConfigurationRoot configuration)  





  7. this.connectionString = 


  8. configuration.GetConnectionString(  


  9. "CommerceConnectionString"); 





  10. public IProductRepository Create() 





  11. //使用位于不同程序集中的依赖项创建一个新的IProductRepositoryFactory 


  12. return new SqlProductRepository(  


  13. new AspNetUserContextAdapter(), 


  14. new CommerceContext(this.connectionString)); 








反模式 DI anti-patterns的更多相关文章

  1. Python编程中的反模式

    Python是时下最热门的编程语言之一了.简洁而富有表达力的语法,两三行代码往往就能解决十来行C代码才能解决的问题:丰富的标准库和第三方库,大大节约了开发时间,使它成为那些对性能没有严苛要求的开发任务 ...

  2. 重构24-Remove Arrowhead Antipattern(去掉箭头反模式)

    基于c2的wiki条目.Los Techies的Chris Missal同样也些了一篇关于反模式的post.  简单地说,当你使用大量的嵌套条件判断时,形成了箭头型的代码,这就是箭头反模式(arrow ...

  3. ORM 是一种讨厌的反模式

    本文由码农网 – 孙腾浩原创翻译,转载请看清文末的转载要求,欢迎参与我们的付费投稿计划! (“Too Long; Didn’t Read.”太长不想看,可以看这段摘要 )ORM是一种讨厌的反模式,违背 ...

  4. Apache Hadoop最佳实践和反模式

    摘要:本文介绍了在Apache Hadoop上运行应用程序的最佳实践,实际上,我们引入了网格模式(Grid Pattern)的概念,它和设计模式类似,它代表运行在网格(Grid)上的应用程序的可复用解 ...

  5. 开发反模式 - SQL注入

    一.目标:编写SQL动态查询 SQL常常和程序代码一起使用.我们通常所说的SQL动态查询,是指将程序中的变量和基本SQL语句拼接成一个完整的查询语句. string sql = SELECT * FR ...

  6. 开发反模式(GUID) - 伪键洁癖

    一.目标:整理数据 有的人有强迫症,他们会为一系列数据的断档而抓狂. 一方面,Id为3这一行确实发生过一些事情,为什么这个查询不返回Id为3的这一行?这条记录数据丢失了吗?那个Column到底是什么? ...

  7. 查询反模式 - 正视NULL值

    一.提出问题 不可避免地,我们都数据库总有一些字段是没有值的.不管是插入一个不完整的行,还是有些列可以合法地拥有一些无效值.SQL 支持一个特殊的空值,就是NULL. 在很多时候,NULL值导致我们的 ...

  8. SQL反模式学习笔记1 开篇

    什么是“反模式” 反模式是一种试图解决问题的方法,但通常会同时引发别的问题. 反模式分类 (1)逻辑数据库设计反模式 在开始编码之前,需要决定数据库中存储什么信息以及最佳的数据组织方式和内在关联方式. ...

  9. SQL反模式学习笔记5 外键约束【不用钥匙的入口】

    目标:简化数据库架构 一些开发人员不推荐使用引用完整性约束,可能不使用外键的原因有一下几点: 1.数据更新有可能和约束冲突: 2.当前的数据库设计如此灵活,以至于不支持引用完整性约束: 3.数据库为外 ...

  10. SQL反模式学习笔记3 单纯的树

    2014-10-11 在树形结构中,实例被称为节点.每个节点都有多个子节点与一个父节点. 最上层的节点叫做根(root)节点,它没有父节点. 最底层的没有子节点的节点叫做叶(leaf). 中间的节点简 ...

随机推荐

  1. 一文总结Java\JDK 17发布的新特性

    ​简介: JDK 17已经于2021年3月16日如期发布.本文介绍JDK 17新特性.JDK 17于2021年9月14日正式发布(General-Availability Release).JDK 1 ...

  2. [FAQ] Member "address" not found or not visible after argument-dependent lookup in address payable.

    顾名思义,address 属性不存在,请检查调用方. 比如:msg.sender.address 会有此提示,在 Solidity Contract 中,msg.sender.balance 是存在的 ...

  3. Util 应用框架 UI 全新升级

    Util UI 已经开发多年, 并在多家公司的项目使用. 不过一直以来, Util UI 存在一些缺陷, 始终未能解决. 最近几个月, Util 团队下定决心, 终于彻底解决了所有已知缺陷. Util ...

  4. 使用.NET源生成器(SG)实现一个自动注入的生成器

    DI依赖注入对我们后端程序员来说肯定是基础中的基础了,我们经常会使用下面的代码注入相关的service services.AddScoped<Biwen.AutoClassGen.TestCon ...

  5. rails 写入日志函数

    json_object={ "ip"=> "127.0.0.1", "ports"=> '80,135', "data ...

  6. uniapp中使用极光推送

    1.注册极光账号 2.注册几个主流手机厂商的开发者账号(注册手机厂商,可以保证app进程不在的时候走厂商通道推送消息) 3.配置uniapp极光插件 https://ext.dcloud.net.cn ...

  7. 远程桌面使用Pr剪视频

    要远程访问高性能计算机并使用 Pr(Adobe Premiere Pro)进行视频编辑,您可以考虑使用流畅且响应迅速的远程桌面软件.您可以考虑以下选项. Splashtop Business Acce ...

  8. 使用 JS 实现在浏览器控制台打印图片 console.image()

    在前端开发过程中,调试的时候,我门会使用 console.log 等方式查看数据.但对于图片来说,仅靠展示的数据与结构,是无法想象出图片最终呈现的样子的. 虽然我们可以把图片数据通过 img 标签展示 ...

  9. AIRIOT物联网平台助力油库自动化升级 实现业务场景全覆盖

      随着我国石油工业的飞速发展,油库规模迅速扩大,油库系统逐渐完善起来.石油行业属于高风险行业,所以石油化工产品在储存.运输和生产各个环节,均有极高的安监.环保.应急的管理要求.通常情况下,油库容量. ...

  10. FFmpeg开发笔记(二十二)FFmpeg中SAR与DAR的显示宽高比

    ​<FFmpeg开发实战:从零基础到短视频上线>一书提到:通常情况下,在视频流解析之后,从AVCodecContext结构得到的宽高就是视频画面的宽高.然而有的视频文件并非如此,如果按照A ...