依赖注入模式与反模式

依赖注入模式与反模式

依赖注入模式

构造器注入

最重要的DI模式。
构造器注入算法

  1. 如何工做:一个私有的只读引用指向依赖,一个带参的构造器初始化该引用。数据库

    Tip1: 保持构造器逻辑的简洁,不要包含其它的逻辑。
    Tip2: 能够把构造器注入当作是静态地声明了类的依赖。明确指明了依劣势赖的类型。编程

  2. 什么时候使用:应该是你默认的DI选择。安全

    Tip1: 若是能够将构造器设计为单一的。重载的构造器会给DI容器形成误导。框架

  3. 优缺点:ide

优点 劣势
最容易实现的DI模式 有些框架假定你有默认构造器,所以使用它很困难;另外一个显见的劣势是在程序初始化时就须要整个依赖图,不过不用担忧出现性能问题。

属性注入

适用于有本地默认依赖,而且但愿开发扩展的场景。
属性注入函数

  1. 如何工做: 一个可写的属性性能

    Tip1 : 又叫作依赖设置器注入,须要为属性设置一个默认值。
    Tip2 : 若是容许在类的声明周期中切换依赖,能够经过在内部引入一个flag来确保依赖只容许被设置一次。测试

  2. 什么时候使用: 用于依赖是可选的状况网站

    Tip1 : 若是你只想留一个扩展点,那么建议你使用Null-Object模式来实现本地默认属性的初始化。
    Tip2 : 若是你想要保留默认的属性,还想要更多的扩展,那么可使用观察者模式或者组合模式。

  3. 优缺点:

优点 劣势
容易理解 健壮地实现它不太容易,客户端可能忘记设置依赖,也可能设置null,另外客户端在类声明周期内改变依赖是也会致使不一致或不指望的行为,这些状况都须要本身处理。
  1. 示例:
 
 
 
 
private CurrencyProfileService currencyProfileService;public CurrencyProfileService CurrencyProfileService{ get { if (this.currencyProfileService == null) { this.CurrencyProfileService = new DefaultCurrencyProfileService(this.HttpContext);//默认值的延迟初始化 } return this.currencyProfileService; } set { if (value == null) { throw new ArgumentNullException("value"); } if (this.currencyProfileService != null) { throw new InvalidOperationException();//只容许依赖定义一次 } this.currencyProfileService = value; }}

方法注入

每个方法的依赖都不相同时。
方法注入

  1. 如何工做:依赖作为一个方法参数

    Tip1 : 首先应该确保传入的依赖非空。
    Tip2 : 若是方法并不使用传入的依赖,最好将参数删去,若是是实现的接口方法,那么应该将参数验证去掉。

  2. 什么时候使用:每一个方法的依赖都不相同时

    Tip : 方法注入和使用抽象工厂模式很类似,抽象工厂的抽象输入能够看做是方法注入。

  3. 优缺点:

优势 缺点
容许方法调用提供指定的上下文环境 适用性不广

上下文环境注入

为每个模块提供依赖,而不用关注每个API。
上下文环境注入

  1. 如何工做: 经过一个静态属性或者方法
 
 
 
 
public string GetMessage(){ return SomeContext.Current.SomeValue;}

也就是说,上面的Current必须是静态的、抽象的、可写的。SomeContext可能的这么实现:

 
 
 
 
public abstract class SomeContext{ public static SomeContext Current { get { var ctx = Thread.GetData(Thread.GetNamedDataSlot("SomeContext")) as SomeContext;//一、从TLS(线程本地存储)得到上下文 if (ctx == null) { ctx = SomeContext.Default; Thread.SetData(Thread.GetNamedDataSlot("SomeContext"), ctx); } return ctx; } set { Thread.SetData(Thread.GetNamedDataSlot("SomeContext"), value);//二、在TLS中保存上下文 } }public static SomeContext Default = new DefaultContext();public abstract string SomeValue { get; }//三、上下文承载的数据}

注意:上面的例子为了简单没有考虑线程安全。自行实现时必定要考虑。
Tip1:该模式与线程和调用上下文并无关系,不少时候,使它在整个应用程序域中Static便可。

  1. 什么时候使用:存在会污染全部API的横切的关注点
    举个例子,你可能为某个函数传入了额外的参数,由于你不知道什么时候可能会用到它。
 
 
 
 
public string GetSomething(SomeService service, TimeProvider timeProvider){ return service.GetStuff("Foo", timeProvider);}

其实上面的GetStuff()方法根本用不到timeProvider,额外的参数污染了API。

 
 
 
 
public string GetStuff(string s, TimeProvider timeProvider){ return this.Stuff(s);}
  1. 使用条件:

    • 须要请求式的上下文: 若是只是须要一些数据(上下文中的全部方法都返回void),那么使用拦截器是更好的解决方案。常见的例子有,日志、度量性能、断言安全上下文,全部这些动做均可以使用拦截器。你只有在须要询问得到某些值时,才考虑使用上下文环境注入。|
    • 存在合适的本地默认依赖: 有隐式的上下文环境存在,即便不显式的分配上下文,也能够顺利地工做。
    • 必须确保上下文的可访问性: 即便存在隐式的上下文环境,仍是应该确保上下文环境非Null。
  2. 优缺点

优点 劣势
不会污染API 含蓄(容易引入潜在的Bug),很难被正确地实现,没法经过接口定义来知道类的依赖关系,也不容易发现类的扩展点
老是能够得到依赖 在一些运行时环境中不能很好的工做(好比须要切换线程上下文时)
  1. 示例
 
 
 
 
public abstract class TimeProvider{ private static TimeProvider current; static TimeProvider() { TimeProvider.current = new DefaultTimeProvider();//一、默认实现 } public static TimeProvider Current { get { return TimeProvider.current; } set { if (value == null)//二、确保非Null { throw new ArgumentNullException("value"); } TimeProvider.current = value; } } public abstract DateTime UtcNow { get; }//三、获取数据 public static void ResetToDefault() { TimeProvider.current = new DefaultTimeProvider(); }}

默认实现:

 
 
 
 
public class DefaultTimeProvider : TimeProvider{ public override DateTime UtcNow { get { return DateTime.UtcNow; } }}

依赖注入反模式

控制狂

与控制反转相反,描述一个类维护了它全部的依赖。最多见的反模式,使用了太多的new关键字,以至咱们须要控制太多实例的声明周期。模块都牢牢耦合在一块儿。

  1. 反例1:直接new
 
 
 
 
private readonly ProductRepository repository;public ProductService(){ string connectionString = ConfigurationManager.ConnectionStrings["CommerceObjectContext"].ConnectionString; this.repository = new SqlProductRepository(connectionString);//直接建立了实例,紧密的耦合关系。}
  1. 反例2:工厂
    最多见的想要解决new实例问题的尝试,主要是选择一些工厂模式。它们存在哪些问题呢?

    • 简单工厂:彻底没有解决DI问题,仅仅是把它移到了具体的工厂实例。咱们仍然不能在运行时更换依赖的实例。
    • 抽象工厂:任然不会解决DI问题,只不过把对具体产品实例的依赖,替换为对具体工厂实例的依赖。
    • 静态工厂:使原有的依赖关系更复杂了。
  2. 重构

    • 首先,确保你是面向接口编程的;
    • 如过你在多个地方建立了特定的依赖,把它们移到一个方法。确保该方法返回的是抽象类型。
    • 是由一种DI模式改造代码,好比构造器注入。

Bastard注入

包括BCL在内,不少.NET代码都包含重载的构造器。这个重载带来了一些负面的影响——默认的构造器实现可能并非返回本地依赖而是一个外部依赖。当你彻底拥抱依赖注入时,这些重载都变为是多余的。

  1. 反例:默认构造函数带来的外部依赖
 
 
 
 
private readonly ProductRepository repository;public ProductService() : this(ProductService.CreateDefaultRepository()) {}//默认构造函数public ProductService(ProductRepository repository)//构造器注入{ if (repository == null) { throw new ArgumentNullException("repository"); } this.repository = repository;}private static ProductRepository CreateDefaultRepository(){ string connectionString = ConfigurationManager .ConnectionStrings["CommerceObjectContext"].ConnectionString; return new SqlProductRepository(connectionString);}

外部依赖

  1. 分析
    这种反模式常常可见,不少开发者没有彻底理解DI,为了类的可测试性选择了这种反模式。这种模式带来了一些糟糕的影响。最重要的就是外部依赖的引入使模块重用变得困难,同时并行开发也牢牢的依赖在一块儿。

  2. 重构

受限的构造

最多见的限制是要求全部的依赖都必须有特定签名的构造器,用来从配置文件来实现延迟绑定。

  1. 反例:
 
 
 
 
string connectionString = ConfigurationManager. ConnectionStrings["CommerceObjectContext"].ConnectionString;string productRepositoryTypeName = ConfigurationManager.AppSettings["ProductRepositoryType"];var productRepositoryType = Type.GetType(productRepositoryTypeName, true);var repository = (ProductRepository)Activator. CreateInstance(productRepositoryType, connectionString);//使用反射建立实例

这个例子中,从配置文件中读取了链接字符串等一系列信息,最后反射时使用了这些信息,实际上隐式地约束了被依赖项。
约束对灵活性的影响是很是大的,好比咱们可能须要将一个单例注入不一样的模块。

  1. 重构:
    使用抽象工厂模式,将类型定义从核心应用中分离开来,如此一来每次从新编译的代码变成一个个程序集。虽然这是一种可行的方案,可是仍然比不上使用DI容器方便。

服务查找器

许多开发者将静态工厂上升到另外一个级别——服务定位器——直接控制依赖。它是模式仍是反模式是见仁见智的。DI容器和服务查找器很像,它们之间的区别是微妙的,关键不在于它是如何实现的,而在于你如何使用它。本质上讲,若是用来在代码基上处理完整的依赖图,那么它是合适的,若是在任什么时候候获取小颗粒的服务,那么它是反模式。

  1. 反例:
 
 
 
 
public static class Locator{ private readonly static Dictionary<Type, object> services = new Dictionary<Type, object>(); public static T GetService<T>()//获取服务 { return (T)Locator.services[typeof(T)]; } public static void Register<T>(T service)//注册服务 { Locator.services[typeof(T)] = service; } public static void Reset()//清空服务 { Locator.services.Clear(); }}

这个反模式看起来很不错,不过它是一个危险的模式。它惟一重要的问题是影响了它的消费者类的可重用性(它包含了冗余的依赖,它不是自描述的)。想象一下两个模块都实现了服务查找器,或者一个使用DI,另外一个使用服务查找器。其实有更好的选择——好比构造器注入。

  1. 重构:
    • 使依赖从一个方法建立;
    • 引入一个readonly字段来保存依赖;
    • 引入带参数的构造器。

注意: 服务查找器和环境上下文模式很像,区别在于本地默认值的可用性上。后者可以保证老是返回一个合适的被请求的服务,一般只有一个。而前者是不能保证的,本质上它使用了弱类型的容器。

DI重构

将运行时的值映射到抽象

  1. 问题:如何处理运行时的值的依赖
    使用构造器注入时,实际上要求咱们在设计时明确实际的依赖,可是有些状况下是不能知足的,好比地图网站,运行时依赖哪一个路径算法,最短路径,最少时间,仍是最少换乘。实际的依赖在运行时才可以肯定。

  2. 解决:抽象工厂
    抽象工厂模式解决的问题就是咱们能够请求抽象的实例,它为抽象类型和具体运行时实例直接提供了一个桥接。

  3. 示例:路径算法

 
 
 
 
public enum RouteType//路径类型{ Shortest = 0, Fastest, Scenic}
 
 
 
 
public interface IRouteAlgorithmFactory//工厂{ IRouteAlgorithm CreateAlgorithm(RouteType routeType);}
 
 
 
 
public IRoute GetRoute(RouteSpecification spec, RouteType routeType){ IRouteAlgorithm algorithm = this.factory.CreateAlgorithm(routeType);//映射运行时的值 return algorithm.CalculateRoute(spec);//使用映射的算法}

使用短生命的依赖

  1. 问题:请求外部资源
    典型的好比数据库链接、Web服务、资源释放等。对于ADO.NET来讲,这些都已是常识,不过对于WCF客户端来讲,若是不尽快地关闭资源,服务端的压力会很大。

  2. 解决方案:将链接管理隐藏在抽象后面
    一方面,依赖不能运行在内存泄漏的应用中,所以咱们必须尽快关闭链接。另外一方面,依赖也不能处理进程外的通讯,所以构造一个包含Close方法的抽象是有漏洞的。即便继承IDisposable接口,也不过是另外一种Close方法,并不能解决底层的问题。
    幸运地是LINQ to SQL和LINQ to Entities为咱们提供了思路——咱们经过context(包含链接的上下文)访问数据。
    链接上下文
    消费者类调用IResource接口定义的方法,链接管理由IResource的实例进行管理。
    毫无疑问,上面的方案抽象粒度比较粗,灵活性不足,有时咱们须要对依赖的生命周期进行更明确地控制,以防内存泄漏。最多见的方案就是IDisposable模式——咱们建立链接、使用链接、释放链接。
    固然咱们可使用实现了IDisposable模式的抽象工厂,只是消费者类必须记得释放资源。

    其实最佳实践是使用C#的using关键字。

解决循环依赖

  1. 问题:不可避免的循环依赖
    只有程序存在循环的依赖关系,咱们是不可能知足全部的依赖的,所以程序也不可能运行。大多数状况下,应该是你程序设计的问题,某些特定的实现带来了循环依赖。若是这种实现不是必须的,你最好对程序从新设计。
    典型的状况是分层应用中循环依赖。
    分层应用中的循环依赖

  2. 解决方案:
    解决的第一步就是打破循环:大多数分层应用中的循环依赖是结构性错误,先仔细考虑下分层是否合理。思考一下循环依赖为何发生,有时能够改变设计,还可使用事件、观察者模式,实在不行最后的方法是将构造器依赖注入改成属性注入。

    将B的DI方式由构造器注入改成属性注入:

 
 
 
 
var b = new B();var a = new A(b);b.C = new C(new D(a));//属性注入

若是你不想或者不能修改B的构造方式,还能够引入一个虚拟的协议:

 
 
 
 
var lb = new LazyB();//和B实现同样的接口var a = new A(lb);lb.B = new B(new C(new D(a)));
  1. 示例:WPF MVVM模式中的例子

    Window依赖于一个ViewModel,而ViewModel依赖于一个IWindow接口,这个接口由WindowAdapter实现。
    MVVM是怎么作的?它使Window和ViewModel的依赖经过属性注入。
 
 
 
 
private void EnsureInitialized(){ if (this.initialized) { return; } var vm = this.vmFactory.Create(this);//建立ViewModel this.WpfWindow.DataContext = vm;//属性注入 this.DeclareKeyBindings(vm); this.initialized = true;}

能够在应用程序根部装配:

 
 
 
 
IMainWindowViewModelFactory vmFactory = new MainWindowViewModelFactory(agent);Window mainWindow = new MainWindow();IWindow w = new MainWindowAdapter(mainWindow, vmFactory);

处理过多的构造器依赖参数

  1. 问题:构造器注入很是容易实现,可是当参数过多时让人很不舒服
 
 
 
 
public MyClass(IUnitOfWorkFactory uowFactory, CurrencyProvider currencyProvider, IFooPolicy fooPolicy, IBarService barService, ICoffeeMaker coffeeMaker, IKitchenSink kitchenSink)

不要把过错归咎于构造器注入,问题在于违反了单一职责原则。

  1. 解决方案:外观模式

    外观隐藏内部的依赖,只提供能够消费的服务。若是系统很是大,能够循环使用该方法

  2. 示例:一个订单服务
    d订单服务的依赖图
    引入两个外观接口:
    引入订单完成外观接口
    引入通知外观接口



相关文章
相关标签/搜索