DI(Dependency injection) 注入方式
这天小明问说
他接手维护的专案中, 从头到尾都一律用 DI 属性注入, 这是很好的设计方式吗?
首先我先简单说明一下, DI 的核心概念是宽鬆耦合, DI 有三种注入的方式:
建构式注入(Constructor Injection)属性注入(Property Injection)方法注入(Method Injection)建构式注入(Constructor Injection)
public class MyHome { IMailService _mailService; public MyHome(IMailService mailService) { _mailService = mailService; }}
以上程式码示範了建构式注入, 当你手动 "new 一个物件" 就必须把 IMailService 物件带进来.
因此该物件在没有这些依赖物件时无法被建立.
属性注入(Property Injection)
public class MyHome { public MyHome() { } public IMailService MailService { get; set; }}
以上程式码示範属性注入, 当你手动 "new 一个物件", 不必一开始就把 IMailService 物件带进来.
这意味着你的物件可以在没有提供这些依赖时正常地工作.
原则上这种方式, 对于不注入相依物件的情况, 物件本身必须做些防护措施, 以免物件执行的时候, 因相依物件参考为 null 而引发 NullReferenceException 异常.
方法注入(Method Injection)
public class MyHome { public MyHome() { } public void Run(IMailService MailService) { ... }}
以上程式码示範方法注入, 当你手动 "new 一个物件", 跟属性注入一样, 也不必一开始就把 IMailService 物件带进来. 只有在用户端呼叫 Run 方法时才需要传入 IMailService 相依物件.
现在我们了解三种注入的方式跟特性, 回头看小明的问题, 他接手的专案中 "一律都用属性注入" ,
以下是小明接手专案中的程式码片段
public class MyHome { public IMailService MailService { get; set; } public Ixxx1 xxx1 { get; set; } public Ixxx2 xxx2 { get; set; } public IxxxN xxxN { get; set; } public void Run() { MailService.Call(); ... }}
以上程式码有几个问题:
对于这个 MyHome 物件採用属性注入, 因为它让依赖不明确, 这意味着在建立物件期间不可能容易地看出依赖关係.这对单元测试来说很重要, 因为你可能想要模拟一些依赖物件.而且 Run 方法里面直接呼叫 MailService 依赖物件, 但 MyHome 被建立的时候, 也没有预设依赖物件的实作.
这会引发 NullReferenceException 异常
另外小明的专案交接人也有疑问说:
那我把所有属性注入通通改成建构式注入, 但是依赖物件超级多, 我不想在建构式看到一堆参数, 所以我才要把这些一堆依赖物件通通改成属性注入阿
遇到这种问题,
我们就应该思考 "当这个物件的建构式需要的参数数量很多的时候",
可能是一种 "程式码坏味道" (Code Smell),
表明您的物件做太多事情, 可能没有遵循单一职责原则.
请考虑将程式码重构为许多相互消耗的较小物件.
结尾
每当需要注入相依物件时, 建议优先考虑 "建构式注入" ,
因为 "new 物件" 的时候, 就要一併传入所有相依物件,
对呼叫端来说相当明确直觉, 马上可以得知这个物件相依于哪些第三方物件.