反模式 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 @ 2024-05-25 23:06  世纪末の魔术师  阅读(2)  评论(0编辑  收藏  举报