現今許多前端或後端的框架,
預設都是採用相依性注入的方式進行開發,
對我這種原先較少使用DI框架的開發者而言,
上手適應的確得花一番功夫。
記得某次參加twMVC週四固定聚會時,
我向Bill叔請益有關相依性注入的看法時,他秒回道:
不就是相依,然後再注入而已。
這看似簡單的幾句話,
卻讓當下只會用DI框架的我,
開始反思何謂相依、何謂注入。
此篇我想從「何謂相依」出發,
分別地來談談「相依」及「注入」,
順手記錄自己理解的過程。
何謂相依
首先,何謂相依?
在教育部的國語辭典查詢的結果顯示:彼此倚賴。
舉個MVC中的Controller與Service的關係而言,
我有個MemberController(以下簡稱Controller),用來控制會員新刪修查的後端流程,
並且有個MemberService(以下簡稱Service),用來處理新刪修查所需的執行細節。
以這個例子而言,
究竟是Controller相依於Service,
還是Service相依於Controller呢?
還是它們兩個的關係是彼此倚賴的?
身為一個專業的工程師,
我相信看程式碼遠比咀嚼文字來的快!
所以我們先來一小段程式碼:
MemberController.cs
public class MemberController : Controller
{
private readonly MemberService _memberService;
public MemberController()
{
_memberService = new MemberService();
}
public MemberController(MemberService memberService)
{
_memberService = memberService;
}
[HttpGet]
public ActionResult Create()
{
return View();
}
[HttpPost]
public ActionResult Create(Member member)
{
_memberService.CreateMember(member);
return View();
}
}
MemberService.cs
public class MemberService
{
public void CreateMember(Member member)
{
//The real detail for creating member ...
}
}
這段程式對各位來說應該再簡單不過,
而我們回到教育部國語辭典的定義:彼此倚賴。
倚賴與相依是同義的,
為避免讀者搞混,以下我換稱為相依。
Controller與Service的關係是否為彼此相依呢?
看起來應該是,但又好像不是。
提到闡述相依,
讓我想起Mariah Carey - Without You中的一段歌詞:
I can't live If living is without you.
也就是「失去你,我也沒辦法獨活」的情節。
有看過蠟筆小新的中生代都知道,
阿吉跟小美是眾所皆知的男女朋友,
如果失去小美,阿吉活不下去,
我們可以稱「阿吉相依於小美」。
這是單方面的相依。
在上述的假設之下,
如果失去阿吉,小美也無法獨活,
我們可以稱「阿吉與小美彼此相依」。
回到正題,
Controller與Service究竟是誰相依於誰?
其實舉個簡單的例子就可以明白了。
如果Service不實作,Controller是否能夠如預期的完成新增會員的任務?
當然不行!
所以我們可以說:Controller相依於Service。
換個角度,如果Controller在新增完會員後拋出錯誤,
是否會影響Service的運作?
[HttpPost]
public ActionResult Create(Member member)
{
_memberService.CreateMember(member);
var a = 0;
var b = 5 / a;
return View();
}
當然不會,Service還是能夠照常運作,
CreateMember依舊會如期執行並完成任務,
除以零所拋出的錯誤不過是導致後續的動作中斷而已。
所以,Service並不相依於Controller。
搞懂相依之後,別急著注入。
讓我們回到現實常態,
一個Controller用到的通常應該不只一個Service而已,
也就是說一個Controller通常相依於多個Service。
相依的對象一多之後,
進行程式碼修改時就容易引起變化,
一不小心就會發生改A壞B的副作用。
提取介面
接著我們幫相依的對象(Service)提取介面,
並透過介面來隔離實作細節,
同時提供替代的可能性。
當然,這會造成開發者必須提取相當多的介面。
也是許多人不太習慣的地方。
但在IDE打天下的年代,
提取介面應該要是幾個按鍵就能完成的,
而不是逐字地敲打。
以上述為例,
我們試著替Service抽取一個介面,
同時讓Controller相依於抽出來的介面。
public class MemberController : Controller
{
private readonly IMemberService _memberService;
public MemberController()
{
_memberService = new MemberService();
}
public MemberController(IMemberService memberService)
{
_memberService = memberService;
}
}
抽完介面,Controller就沒有相依了嗎?
的確,MemberController現在已經不相依於MemberService了,
但它仍然相依於MemberService的實作介面(IMemberService)。
提取介面僅能隔離相依宿主(Controller)與實作者(Service),
並不能完全阻擋變化的發生,
但如若透過介面注入容器,
將控制權反轉,
能夠集中管理所有的實作對象,
並在必要時進行實作對象的抽換,
將修改的範圍限縮到最小,
提取介面另一項好處 – 利於單元測試。
透過介面可以讓我們在感測物件行為時,
隔離不必要的行為變化。
總總利弊相較之下,
在適當的情境提取介面,
我想能帶來的效益是值得的。
結語
內容如有不洽之處,
歡迎指證討論。
2020新春第一發,
也祝所有朋友新年快樂!
===2020/1/28 21:53===
感謝善意人士提醒,
讓我發現自己對ISP理解是錯誤的,
實際上文內提及的範圍僅是提取介面而已(已修正文章)。
==================