介面 & 注入

上篇跟大家簡單的介紹了什麼是耦合,其中有提到依賴反轉原則(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)