上篇跟大家簡單的介紹了什麼是耦合,其中有提到依賴反轉原則(D.I.P.),其中有一點是說高層次的模組不應該依賴於低層次的模組,兩者都應該依賴抽象介面。
高層次的模組不應該依賴於低層次的模組比較好理解,下面這就是一個高階模組依賴低階模組的範例(哪裡違反D.I.P.請參考上篇文章):
public class Module_A
{
var txtLog = new TxtLogHelper();
var log = txtLog.WriteLog();
}
兩者都應該依賴於抽象介面這句話的介面是什麼意思呢?
介面只是一份規格,並未包含任何實作,故任何類別只要實作了這份規格,便能夠與 Module_A銜接,完成發送產出log的工作。有了中間這層介面,開發人員便能夠「針對介面、而非針對實作來撰寫程式。」(program to an interface, not an implementation),使應用程式中的各部元件保持「有點黏、又不會太黏」的適當距離,從而達成寬鬆耦合的目標。就好比一台紅白機,他的卡帶插槽其實就是個介面規格,卡帶才是遊戲的本體(實作),要換遊戲時,應該沒人再買一台新的紅白機插新的卡帶來玩,而是只要換卡帶(實作)就好了。
接著我們來提煉介面(Extract Interface),延續上篇文章來當範例。我們可以觀察TxtLogHelper跟ExcelLogHelper這兩個類別中有何相似之處,可以提煉出來當作介面,我們可以觀察到這兩個類別都有WriteLog()在處理log產出的行為。
public class TxtLogHelper
{
public string WriteLog()
{
return "Write txt log";
}
}
public class ExcelLogHelper
{
public string WriteLog()
{
return "Write excel log";
}
}
因此,我們就把介面寫成這樣
interface ILogService
{
string WriteLog();
}
在修改一下TxtLogHelper跟ExcelLogHelper這兩個類別實作ILogService這個介面
public class TxtLogHelper: ILogService
{
public string WriteLog()
{
return "Write txt log";
}
}
public class ExcelLogHelper: ILogService
{
public string WriteLog()
{
return "Write excel log";
}
}
介面做好之後,我們要到Services(DI容器)去進行註冊(.Net Core已經有內建DI容器了,其他的DI容器套件有Unity. AutoFact…etc)。因為一個介面可以有多個實作,所以註冊講白話一點就是把你的介面跟要實作的類別做mapping。先註冊再注入!!
//.net 6開始可以直接在Program.cs設定容器mapping,.net 6之前要在Startup.cs裡進行註冊
builder.Services.AddScoped<ILogService, TxtLogHelper>();
我們可以把DI容器當做事一個控制反轉(IoC)中心,把原本傳統的高階主動建立低階模組的做法,反轉成被動接受由控制反轉中心產生的低階模組,把控制反轉中心產生的低階模組交給高階模組的這個行為,就是所謂的注入。註冊完之後我們就可以開始進行DI注入了(注入有三種作法:建構式注入、方法注入、屬性注入,.Net Core預設提供建構式注入)。
我們要把原本的高階模組程式
public class Module_A
{
var txtLog = new TxtLogHelper();
var log = txtLog.WriteLog();
}
修改成如下
public class Module_A
{
private ILogService _logService;
public Module_A(ILogService logService)
{
_logService = logService;
}
public ActionResult GetLog()
{
var Log = _logService.WriteLog();
return Content(Log);
}
}
修改完畢後,可以看到我們在建構子注入介面後,我們就由原本的相依於類別改成相依於抽象介面了。完成了D.I.P.中的高層次的模組不應該依賴於低層次的模組,兩者都應該依賴於抽象介面。
上述的行為也稱之為解耦,之後如果又有要變更log產出方式時,可以去DI容器直接替換掉介面要對應的實體類別就可以了。
當然,上面的建構式注入作法有個缺點,就是TxtLogHelper跟ExcelLogHelper只能擇一注入,這樣並不符合現實需求,因為有可能會依照不同的條件而有不同的log產出方式。遇到這種情形時,我們可以改採用如下的[方法注入]。
public class LogHelper
{
public CreateLog(ILogService logService)
{
logService.WriteLog()
}
}
public class Module_A
{
private LogHelper logHelper;
public Module_A()
{
logHelper = new LogHelper();
}
public ActionResult GetLog()
{//for一般員工使用
var txtLogHelper = new TxtLogHelper();
var LogContent = logHelper.CreateLog(txtLogHelper);
return Content(LogContent);
}
public ActionResult GetLog_Boss()
{//for老闆使用
var excelLogHelper = new ExcelLogHelper();
var LogContent = logHelper.CreateLog(excelLogHelper);
return Content(LogContent);
}
}
改成方法注入後,可以看到雖然是有違反D.I.P.原則的地方,但寫程式不可能完全0耦合,我們要做的是減少耦合。以上面程式來說,至少我們在呼叫WriteLog()時,還是透過介面去調用實作方法的。
上面所說的都只是參考各網站文章後整理出來的簡單IoC & DI的基礎知識,有錯誤的地方歡迎留言告知。另外很推薦下面參考網站的第一個連結網址,裡面講解得更深入. 清楚。
Ref:
菜雞新訓記 (6): 使用 依賴注入 (Dependency Injection) 來解除強耦合吧
Dependency Injection 筆記 (2)