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/






留言

這個網誌中的熱門文章

何謂淨重(Net Weight)、皮重(Tare Weight)與毛重(Gross Weight)

Architecture(架構) 和 Framework(框架) 有何不同?_軟體設計前的事前規劃的藍圖概念

經得起原始碼資安弱點掃描的程式設計習慣培養(五)_Missing HSTS Header