Dependency Injection原理與實踐(一)
在目前軟體開發過程中,往往計畫趕不上變化。
需求一而再再而三改
不再可能是等所有規格資訊完整後才開始開發
因此篹體工程師就需要練習撰寫一些
可維護性較高、可調整性高的程式碼來因應需求的頻繁變化
或因應環境升級而導致的第三方套件的不支援(版本只cover4~5年)
在諸多不確定性情境下
而造就的一種程式撰寫手法
Dependency Injection(DI) 目的在於解除物件與物件間,兩者的直接相依關係。
注入又分為
Constructor DI
Property DI
Method DI
在此用一個員工的Model Class、DAL Class的開發來看差別
我們想透過一專門操控員工的業務邏輯層Class
來獲取一員工清單
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DependencyInjectionExample { public class Employee { public int ID { get; set; } public string Name { get; set; } public string Department { get; set; } } public class EmployeeDAL { public List<Employee> GetAllEmployees() { List<Employee> ListEmployees = new List<Employee>(); //Get the Employees from the Database //for now we are hard coded the employees ListEmployees.Add(new Employee() { ID = 1, Name = "Pranaya", Department = "IT" }); ListEmployees.Add(new Employee() { ID = 2, Name = "Kumar", Department = "HR" }); ListEmployees.Add(new Employee() { ID = 3, Name = "Rout", Department = "Payroll" }); return ListEmployees; } } public class EmployeeBL { public EmployeeDAL employeeDAL; public List<Employee> GetAllEmployees() { employeeDAL = new EmployeeDAL(); return employeeDAL.GetAllEmployees(); } } class Program { static void Main(string[] args) { EmployeeBL employeeBL = new EmployeeBL(); List<Employee> lsEmp = employeeBL.GetAllEmployees(); foreach (Employee emp in lsEmp) { Console.WriteLine("ID = {0}, Name = {1}, Department = {2}", emp.ID, emp.Name, emp.Department); } Console.ReadKey(); } } } |
這裡可看到
EmployeeBL Class 必須依賴 EmployeeDAL 中定義的method GetAllEmployees
而這也導致EmployeeBL 跟 EmployeeDAL兩Class是極高耦合(牽一髮動全身)
當一個要改另一個也會需要一起改
(一個情境假設今天跟某公司合併、併購之類的要回收該公司資料庫
要整併一切的員工資料時候、或者不同廠區、分公司的員工清單要額外劃分時候)
Constructor DI
這裡我們透過Constructor DI技巧來解決此情境的問題
我們不希望直接在EmployeeBL Class 指定具體的Class
我們先針對EmployeeDAL 做調整
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public interface IEmployeeDAL { List<Employee> GetAllEmployees(); } public class EmployeeDAL : IEmployeeDAL { public List<Employee> GetAllEmployees() { List<Employee> ListEmployees = new List<Employee>(); //Get the Employees from the Database //for now we are hard coded the employees ListEmployees.Add(new Employee() { ID = 1, Name = "Pranaya", Department = "IT" }); ListEmployees.Add(new Employee() { ID = 2, Name = "Kumar", Department = "HR" }); ListEmployees.Add(new Employee() { ID = 3, Name = "Rout", Department = "Payroll" }); return ListEmployees; } } |
在目前案例EmployeeDAL 屬於我們要注入的物件(dependency object)
因此宣告一Interface 命名為IEmployeeDAL
只要是實作此介面的一律要實作GetAllEmployees的方法
針對EmployeeDAL 去implement 此interface
(在此有個重點就是藉由interface形式來實踐注入才可具有彈性)
在來需要調整EmployeeBL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class EmployeeBL { //public EmployeeDAL employeeDAL; public IEmployeeDAL employeeDAL; public EmployeeBL(IEmployeeDAL employeeDAL) { this.employeeDAL = employeeDAL; } public List<Employee> GetAllEmployees() { //employeeDAL = new EmployeeDAL(); return employeeDAL.GetAllEmployees(); } } |
於Constructor 中傳入Interface
間接去Call GetAllEmployees方法
往後只要是有實作該interface的物件皆可傳入做後續使用
呼叫端程式
1 2 3 4 5 6 7 8 9 10 11 12 13 | class Program { static void Main(string[] args) { EmployeeBL employeeBL = new EmployeeBL(new EmployeeDAL()); List<Employee> lsEmp = employeeBL.GetAllEmployees(); foreach (Employee emp in lsEmp) { Console.WriteLine("ID = {0}, Name = {1}, Department = {2}", emp.ID, emp.Name, emp.Department); } Console.ReadKey(); } } |
Property DI
設定好注入目標鎖定在EmployeeBL中的私有欄位employeeDAL去產生公有的property
並於setter做DI處理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public class EmployeeBL { private IEmployeeDAL employeeDAL; public IEmployeeDAL employeeDataObj { get { if(employeeDataObj == null) { throw new Exception("Employee is not initialized"); } else { return employeeDAL; } } set { employeeDAL = value; } } public List<Employee> GetAllEmployees() { return employeeDAL.GetAllEmployees(); } } |
屬性注入的呼叫端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class Program { static void Main(string[] args) { EmployeeBL employeeBL = new EmployeeBL(); employeeBL.employeeDataObj = new EmployeeDAL(); List<Employee> lsEmp = employeeBL.GetAllEmployees(); foreach (Employee emp in lsEmp) { Console.WriteLine("ID = {0}, Name = {1}, Department = {2}", emp.ID, emp.Name, emp.Department); } Console.ReadKey(); } } |
一樣也可達成更具有彈性獲取員工清單的目的
何時要使用constructor DI 何時要用property DI呢?
從較大宗設計角度來看constructor DI比較可以確保
在呼叫任一dependency object的method之前都有做好初始化的動作(較不會跑null exception)
實際應用也較少用property DI
除非既有物件中有一堆method 只支援objectA
而我需要擴充新method支援objectB的時候
此時若用constructor DI就會變得很辛苦
每個既有在龐大專案中已實體化的物件都必須做修改
Method DI
注入目標鎖定在EmployeeBL中的GetAllEmployees 方法
從外部一樣是傳入只要有實作interface 的物件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | public class EmployeeBL { private IEmployeeDAL employeeDAL; public List<Employee> GetAllEmployees(IEmployeeDAL employeeDAL) { this.employeeDAL = employeeDAL; return this.employeeDAL.GetAllEmployees(); } } class Program { static void Main(string[] args) { EmployeeBL employeeBL = new EmployeeBL(); List<Employee> lsEmp = employeeBL.GetAllEmployees(new EmployeeDAL()); foreach (Employee emp in lsEmp) { Console.WriteLine("ID = {0}, Name = {1}, Department = {2}", emp.ID, emp.Name, emp.Department); } Console.ReadKey(); } } |
當整個class沒有跟需依賴的物件有任何依賴時但其中一個 method必須依賴特定物件時採用。
Ref:
Dependency Injection in C#
https://dotnettutorials.net/lesson/dependency-injection-design-pattern-csharp/
Property and Method Dependency Injection in C#
https://dotnettutorials.net/lesson/setter-dependency-injection-design-pattern-csharp/
留言
張貼留言