反模式 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.  
  4. public ProductService() 
  5. { 
  6. //反模式范例,直接new一个SqlProductRepository,导致紧耦合 
  7. _productRepository = new SqlProductRepository(); 
  8. } 
  9. ... 

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

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

  1. public class ProductRepositoryFactory 
  2. { 
  3. public static IProductRepository CreateProductRepository() 
  4. { 
  5. //工厂模式反模式范例,程序变复杂,只是控制狂换了个位置 
  6. return new SqlProductRepository(); 
  7. } 
  8. } 

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

  1. public static IProductRepository Create() 
  2. { 
  3. IConfigurationRoot configuration = new ConfigurationBuilder() 
  4. .SetBasePath(Directory.GetCurrentDirectory()) 
  5. .AddJsonFile("appsettings.json") 
  6. .Build(); 
  7. string repositoryType = configuration["productRepository"]; 
  8. switch (repositoryType) 
  9. { 
  10. case "sql": return new SqlProductRepository(); 
  11. case "azure": return AzureProductRepository(); 
  12. default: throw new InvalidOperationException("..."); 
  13. } 
  14. } 

IProductRepository
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.  
  11. this.repository = repository;  
  12. } 

2. 服务定位

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

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

  1. 实例化和配置应用程序中的各种对象和组件。
  2. 注册依赖关系,进行依赖注入(Dependency Injection)。
  3. 加载配置设置,设置应用程序的参数和行为。
  4. 协调应用程序中各个部分之间的交互和依赖关系。
    通过将这些逻辑集中在一个地方,即“composition root”,可以实现应用程序的解耦和灵活性,使得应用程序的各个部分可以更容易地被替换、修改或扩展。这种设计模式有助于提高代码的可维护性和可测试性。

2-1. 示例:

  1. ... 
  2. public class ProductService : IProductService 
  3. { 
  4. private readonly IProductRepository repository; 
  5. //无参构造函数,让人看不清楚依赖关系 
  6. public ProductService() 
  7. { 
  8. //通过服务定位组件Locator来获取实例 
  9. this.repository = Locator.GetService<IProductRepository>(); 
  10. } 
  11. public IEnumerable<DiscountedProduct> GetFeaturedProducts() { ... } 
  12. } 
  13. ... 
  14. //简单服务定位的实现 
  15. public static class Locator 
  16. { 
  17. private static Dictionary<Type, object> services =  
  18. new Dictionary<Type, object>(); 
  19. //注册实例 
  20. public static void Register<T>(T service) 
  21. { 
  22. services[typeof(T)] = service; 
  23. } 
  24. //获取实例 
  25. public static T GetService<T>()  
  26. {  
  27. return (T)services[typeof(T)];  
  28. } 
  29. public static void Reset() 
  30. { 
  31. services.Clear(); 
  32. } 
  33. } 
  34. ... 
  35. //以服务定位来进行单元测试 
  36. [Fact] 
  37. public void GetFeaturedProductsWillReturnInstance() 
  38. { 
  39. // Arrange 
  40. var stub = ProductRepositoryStub();  
  41. Locator.Reset();  
  42. Locator.Register<IProductRepository>(stub);  
  43. var sut = new ProductService(); 
  44. // Act 
  45. var result = sut.GetFeaturedProducts();  
  46. // Assert 
  47. Assert.NotNull(result); 
  48. } 

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. 服务定位的坏处

  • 和服务定位器绑定的一些类别可能成为冗余

  • 这个类使它的依赖性变得不明显

    InVS

注意
在组合根区域使用DI容器,并不算服务定位反模式,它是一个基础框架组件。

3. 环境上下文

ambient context即环境上下文。在软件开发中,ambient context 是指一种模式,用于在应用程序中共享环境相关的信息或配置,而无需显式传递这些信息给每个组件或方法。这种模式通常通过线程本地存储(Thread Local Storage)或者类似的机制实现。一般在组合根之外的地方,透过一个全局存取的static修饰子类别成员。
通过 ambient context 模式,可以在整个应用程序中访问共享的环境信息,比如当前用户身份、语言设置、主题样式等,而无需在每个方法或组件中显式地传递这些信息。这种模式的优点在于可以简化代码,提高可维护性,并且避免了在每个组件中传递相同的上下文信息的重复工作。
需要注意的是,虽然 ambient context 可以简化代码,但过度使用它可能会导致代码难以理解和调试,因为它引入了隐式的依赖关系。因此,在使用 ambient context 模式时需要权衡利弊,并遵循良好的设计原则。

3-1. 示例

  1. public string GetWelcomeMessage() 
  2. { 
  3. ITimeProvider provider = TimeProvider.Current;  
  4. DateTime now = provider.Now; 
  5. string partOfDay = now.Hour < 6 ? "night" : "day"; 
  6. return string.Format("Good {0}.", partOfDay); 
  7. } 

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

  1. //查询当前系统时间的管道 
  2. public interface ITimeProvider 
  3. { 
  4. DateTime Now { get; }  
  5. } 
  6. ... 
  7. 静态类提供全局范围可读取实例 
  8. public static class TimeProvider  
  9. { 
  10. //内建预设初始化 
  11. private static ITimeProvider current = 
  12. new DefaultTimeProvider(); 
  13. //全局范围类可对ITimeProvider不稳定依赖进行读取,设置的静态属性成员  
  14. public static ITimeProvider Current  
  15. { 
  16. get { return current; } 
  17. set { current = value; } 
  18. } 
  19. //预设 
  20. private class DefaultTimeProvider : ITimeProvider  
  21. { 
  22. public DateTime Now { get { return DateTime.Now; } } 
  23. } 
  24. } 

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

  1. [Fact] 
  2. public void SaysGoodDayDuringDayTime() 
  3. { 
  4. // Arrange 
  5. DateTime dayTime = DateTime.Parse("2019­01­01 6:00"); 
  6. var stub = new TimeProviderStub { Now = dayTime }; 
  7. //将原本预设替换为测试用替身 
  8. TimeProvider.Current = stub; 
  9. //WelcomeMessageGenerator构造API未揭露其需要ITimeProvier这层关系 
  10. var sut = new WelcomeMessageGenerator();  
  11. // Act 
  12. string actualMessage = sut.GetWelcomeMessage(); //TimeProvider.Current与GetWelcomeMessage时序耦合 
  13. // Assert 
  14. Assert.Equal(expected: "Good day.", actual: actualMessage); 
  15. } 

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

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

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

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

  1. 将这些环境上下文的调用集中到一处,这个订房的绝佳选择就是调用构造器的时候。
  2. 建立一个private readonly私有只读成员,用于存放透过环境上下文索取的依赖对象,之后类别中的所有需要这份以来的程序,都直接引用这个新的私有成员。
    改进后的程序:
  1. public class WelcomeMessageGenerator 
  2. { 
  3. private readonly ITimeProvider timeProvider; 
  4. public WelcomeMessageGenerator(ITimeProvider timeProvider) 
  5. { 
  6. if (timeProvider == null) 
  7. throw new ArgumentNullException("timeProvider"); 
  8. this.timeProvider = timeProvider; 
  9. } 
  10. public string GetWelcomeMessage() 
  11. { 
  12. DateTime now = this.timeProvider.Now; 
  13. ... 
  14. } 
  15. } 

4. 限制性构造

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

4-1. 示例

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

  1. public class SqlProductRepository : IProductRepository 
  2. { 
  3. public SqlProductRepository(string connectionStr)  
  4. { 
  5. } 
  6. } 
  7. public class AzureProductRepository : IProductRepository 
  8. { 
  9. public AzureProductRepository(string connectionStr) 
  10. { 
  11. } 
  12. } 

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

  1. string connectionString = this.Configuration  
  2. .GetConnectionString("CommerceConnectionString");  
  3. var settings =  
  4. this.Configuration.GetSection("AppSettings");  
  5.  
  6. string productRepositoryTypeName =  
  7. settings.GetValue<string>("ProductRepositoryType"); 
  8. var productRepositoryType =  
  9. Type.GetType(  
  10. typeName: productRepositoryTypeName,  
  11. throwOnError: true);  
  12. var constructorArguments = 
  13. new object[] { connectionString }; 
  14. IProductRepository repository =  
  15. (IProductRepository)Activator.CreateInstance(  
  16. productRepositoryType, constructorArguments); 
  17.  
  18. ... 
  19. { 
  20. "ConnectionStrings": { 
  21. "CommerceConnectionString": 
  22. "Server=.;Database=MaryCommerce;Trusted_Connection=True;" 
  23. }, 
  24. "AppSettings": { 
  25. "ProductRepositoryType": "SqlProductRepository, Commerce.SqlDataAccess" 
  26. }, 
  27. } 

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

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

虽然类似上面代码中这种限制最普遍,但在灵活性方面的成本是巨大的。无论您如何约束对象构造,您都会失去灵活性。
当您有多个类需要相同的依赖关系时,您可能希望在所有这些类之间共享一个实例。只有当您可以从外部注入该实例时,这才有可能实现。尽管您可以在每个类中编写代码,以从配置文件中读取类型信息并使用Activator.CreateInstance。 CreateInstance来创建正确的实例类型,它确实需要以这种方式共享单个实例。相反,同一类的多个实例会占用更多的内存。

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

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

ABFac
ABFac

  1. public class SqlProductRepository : IProductRepository 
  2. { 
  3. private readonly IUserContext userContext; 
  4. private readonly CommerceContext dbContext; 
  5. public SqlProductRepository( 
  6. IUserContext userContext, CommerceContext dbContext) 
  7. { 
  8. if (userContext == null) 
  9. throw new ArgumentNullException("userContext"); 
  10. if (dbContext == null) 
  11. throw new ArgumentNullException("dbContext"); 
  12. this.userContext = userContext; 
  13. this.dbContext = dbContext; 
  14. } 
  15. } 

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

  1. public class SqlProductRepositoryFactory 
  2. : IProductRepositoryFactory 
  3. { 
  4. private readonly string connectionString; 
  5.  
  6. //传入Mircorsoft的IConfigurationRoot让后读取需要的设定值。玩意要是设定值不存在,会在构建时抛出异常 
  7. public SqlProductRepositoryFactory( 
  8. IConfigurationRoot configuration)  
  9. { 
  10. this.connectionString = 
  11. configuration.GetConnectionString(  
  12. "CommerceConnectionString"); 
  13. } 
  14. public IProductRepository Create() 
  15. { 
  16. //使用位于不同程序集中的依赖项创建一个新的IProductRepositoryFactory 
  17. return new SqlProductRepository(  
  18. new AspNetUserContextAdapter(), 
  19. new CommerceContext(this.connectionString)); 
  20. } 
  21. } 
posted @   世纪末の魔术师  阅读(19)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
  1. 1 ありがとう··· KOKIA
ありがとう··· - KOKIA
00:00 / 00:00
An audio error has occurred.

作词 : KOKIA

作曲 : KOKIA

编曲 : 日向敏文

作词 : KOKIA

作曲 : KOKIA

誰もが気付かぬうちに

誰もが気付かぬうちに

何かを失っている

フッと気付けばあなたはいない

思い出だけを残して

せわしい時の中

言葉を失った人形達のように

街角に溢れたノラネコのように

声にならない叫びが聞こえてくる

もしも もう一度あなたに会えるなら

もしも もう一度あなたに会えるなら

たった一言伝えたい

ありがとう

ありがとう

時には傷つけあっても

時には傷つけあっても

あなたを感じていたい

思い出はせめてもの慰め

いつまでもあなたはここにいる

もしも もう一度あなたに会えるなら

もしも もう一度あなたに会えるなら

たった一言伝えたい

ありがとう

ありがとう

もしも もう一度あなたに会えるなら

もしも もう一度あなたに会えるなら

たった一言伝えたい

もしも もう一度あなたに会えるなら

たった一言伝えたい

ありがとう

ありがとう

時には傷つけあっても

時には傷つけあっても

あなたを感じてたい

点击右上角即可分享
微信分享提示