Dependency Injection là gì ?
Dependency Injection (gọi tắt DI) là một mẫu thiết kế (Design Pattern) được sử dụng trong lập trình để triển khai cơ chế Inversion of Control (hay gọi là IoC). DI giúp quản lý sự phụ thuộc giữa các đối tượng, cho phép tạo ra các đối tượng phụ thuộc ở bên ngoài một lớp và cung cấp các đối tượng phụ thuộc này đến một lớp bằng nhiều cách. Hay nói cách khác, DI sẽ giúp chúng ta tạo ra sự kết nối lỏng lẻo (loosely-coupled) giữa các đối tượng trong khi viết mã, giúp việc bảo trì và kiểm thử dễ dàng hơn vì tổ chức code sẽ có dạng Modular. DI giảm thiểu hard-coded bằng cách tiêm các phụ thuộc (Dependency Injection) ở thời điểm Runtime thay vì thời điểm Compile.
Các kiểu lớp cơ sở của một cơ chế DI
Để tổ chức một cơ chế DI thì bạn cần sự có mặt của 3 kiểu Class sau:
Client Class: hay gọi là lớp phụ thuộc (dependent class) nó sẽ chứa các đối tượng phụ thuộc của Service class.
Service Class: bản thân lớp này là một đối tượng phụ thuộc (dependency) cung cấp các đối tượng phụ thuộc cho Client Class.
Injector Class: lớp này có nhiệm vụ tiêm đối tượng phụ thuộc (Service Class) đến một lớp phụ thuộc (Client Class) hay gọi là lớp hành vi, chúng sẽ quyết định phụ thuộc nào được vận chuyển đến Client Class.
Các kiểu tiêm phụ thuộc (Dependency Injection)
Như bạn đã thấy, Injector Class có nhiệm vụ tiêm các phụ thuộc (Service Class) vào trong một lớp phụ thuộc (Client Class), lúc này có 3 cách để Injector Class tiêm các phụ thuộc. Chúng ta sẽ cùng tìm hiểu về 3 cách tiêm phụ thuộc
Constructor Injection: phương pháp này Injector sẽ cung cấp Service (dependency) thông qua Constructor của Client.
Property Injection: phương pháp này còn gọi là Setter Injection, Injector sẽ cung cấp Service (dependency) thông qua một public property của Client.
Method Injection: phương pháp này tạo ra một Interface Injection chưa một phương thức có tham số tham chiếu đến đối tượng phụ thuộc, Interface này sau đó được triển khai ở lớp Client (dependent class).
Để hiểu rõ hơn chúng ta sẽ cùng đi qua một số ví dụ
Phương pháp Constructor Injection
Ở phương pháp này, chúng ta sẽ tạo ra một ứng dụng nhỏ về lấy thông tin của học sinh (Students) và giáo viên (Teachers).
Service Class
Về cơ cấu tổ chức code sẽ bao gồm một interface có tên là IPersonInfo, hai lớp Service có tên StudentInfo, TeacherInfo có nhiệm vụ lấy thông tin về tên, ngày sinh, chức vụ của một cá nhân. Ví dụ, ở lớp StudentInfo có một người tên là Mike, ngày sinh 12/5/2000 và là một học sinh, ở lớp TeacherInfor có một người tên là Jame Sean, sinh ngày 12/5/1988 và là một giáo viên.
Client Class
Tiếp theo, chúng ta có lớp SchoolOperation đây gọi là lớp Client (dependent), lớp này có nhiệm vụ nhận các phụ thuộc (dependency) từ một Injector thông qua Constructor của nó. Chúng ta thấy rằng SchoolOperation có một Constructor có một tham số kiểu IPersonInfo và chúng ta nói rằng tham số này chính là một phụ thuộc sẽ được truyền đến khi chương trình chạy lên, và chúng ta sẽ tham chiếu đến nó để thực hiện các hoạt động của phụ thuộc này bao gồm các phương thức GetName(), GetDOB(), GetRole().
Injector Class
Cuối cùng, ta sẽ tạo một lớp có vài trò là một Injector và ở đây có chính là lớp Program, lớp này sẽ có thêm vụ tạo ra các phụ thuộc từ các lớp Service và truyền vào Constructor của lớp Client
namespace TypeOfDI
{
public interface IPersonInfo
{
string GetName(int id);
DateOnly GetDOD(int id);
string GetRole(int id);
}
public class StudentInfo : IPersonInfo
{
public string GetName(int id) // Tên
{
return "Mike";
}
public DateOnly GetDOD(int id) // Ngày sinh
{
return new DateOnly(2000, 12, 05);
}
public string GetRole(int id) //Chức vụ
{
return "Student";
}
}
public class TeacherInfo : IPersonInfo
{
public DateOnly GetDOD(int id)
{
return new DateOnly(1988, 12, 05);
}
public string GetName(int id)
{
return "Jame Sean";
}
public string GetRole(int id)
{
return "Teacher";
}
}
public class SchoolOperation
{
private readonly IPersonInfo _personInfo;
public SchoolOperation(IPersonInfo personInfo)
{
_personInfo = personInfo;
}
public void GetInfo(int id)
{
var name = _personInfo.GetName(id);
Console.WriteLine($"Person's name with ID {id}: " + name);
var dob = _personInfo.GetDOD(id);
Console.WriteLine($"Person's Date of Birth with ID {id}: " + dob);
var role = _personInfo.GetRole(id);
Console.WriteLine($"Person's role with ID {id}: " + role);
}
}
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Do you want to display student information (1) or teacher information (2)? ");
var userSelection = Console.ReadLine();
IPersonInfo? personInfo = null;
switch (userSelection)
{
case "1":
personInfo = new StudentInfo();
break;
case "2":
personInfo = new TeacherInfo();
break;
default: break;
}
if (personInfo != null)
{
SchoolOperation schoolOpr = new(personInfo);
schoolOpr.GetInfo(5);
}
Console.ReadKey();
}
}
}
Kêt quả chạy chương trình: nhập 1 nếu muốn hiển thị thông tin học sinh và 2 nếu muốn hiển thị thông tin giáo viên
Do you want to display student information (1) or teacher information (2)?
1
Person's name with ID 5: Mike
Person's Date of Birth with ID 5: 12/5/2000
Person's role with ID 5: Student
Kết luận: như các bạn thấy các lớp Service (StudentInfo, TeacherInfo) và lớp Client (SchoolOperation) sẽ kết hợp với nhau thông qua một phụ thuộc đó là một tham chiếu Interface mà không cần phải là một đối tượng cụ thể của các lớp Service (ví dụ: new StudentInfo(), new TeacherInfo()). Lớp Client (SchoolOperation) sẽ không tạo ra bất cứ một đối tượng cụ thể nào của lớp Service nào ở trong nó cả, điều này tạo ra một loosely-coupled giữa các đối tượng với nhau, chúng sẽ giảm thiểu sự phụ thuộc vào nhau và nếu bạn muốn mở rộng ra nhiều các đối tượng Service khác để thay thế cho Service cũ một cách nhanh chóng, bạn sẽ không phải thay đổi code trong lớp Client mà chỉ đơn giản là thay thế các phụ thuộc bạn muốn tiêm vào Client trong lớp Injector.
Phương pháp tiêm phụ thuộc bằng Constructor cũng là phương pháp đơn giản và phổ biển nhất hiện nay, được khuyên dùng. Tuy nhiên tùy theo phong cách code và tính chất của một dự án mà bạn còn có thể dùng các phương pháp khác, hãy xem bên dưới để tìm hiểu 2 cách tiếp cận tiêm phụ thuộc khác nhé.
Phương pháp Property Injection
Ở phương pháp này, chúng ta sẽ thay đổi cách truyền phụ thuộc và vẫn giữa nguyên bài toán của ví dụ Constructor Injection phía trên. Bây giờ lớp SchoolOperation sẽ có một Property tên là PersonInfo và chúng ta sẽ không dùng Constructor nào cả. Bước tiếp theo ở lớp Injector (lớp Program) lớp này sẽ tạo ra phụ thuộc và tiêm phụ thuộc này vào Property PersonInfo (với phương thức Setter). Lúc này bài toán sẽ có kết quả tương tự như phương pháp Constructor Injection. Hãy nhớ, Property Injection là phương pháp giúp cho lớp Client sẽ nhận được một phụ thuộc từ bên ngoài bằng Property của nó.
namespace TypeOfDI
{
public interface IPersonInfo
{
string GetName(int id);
DateOnly GetDOD(int id);
string GetRole(int id);
}
public class StudentInfo : IPersonInfo
{
public string GetName(int id)
{
return "Mike";
}
public DateOnly GetDOD(int id)
{
return new DateOnly(2000, 12, 05);
}
public string GetRole(int id)
{
return "Student";
}
}
public class TeacherInfo : IPersonInfo
{
public DateOnly GetDOD(int id)
{
return new DateOnly(1988, 12, 05);
}
public string GetName(int id)
{
return "Jame Sean";
}
public string GetRole(int id)
{
return "Teacher";
}
}
public class SchoolOperation
{
public IPersonInfo? PersonInfo { get; set; }
public void GetInfo(int id)
{
var name = PersonInfo?.GetName(id);
Console.WriteLine($"Person's name with ID {id}: " + name);
var dob = PersonInfo?.GetDOD(id);
Console.WriteLine($"Person's Date of Birth with ID {id}: " + dob);
var role = PersonInfo?.GetRole(id);
Console.WriteLine($"Person's role with ID {id}: " + role);
}
}
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Do you want to display student information (1) or teacher information (2)? ");
var userSelection = Console.ReadLine();
IPersonInfo? personInfo = null;
switch (userSelection)
{
case "1":
personInfo = new StudentInfo();
break;
case "2":
personInfo = new TeacherInfo();
break;
default: break;
}
if (personInfo != null)
{
SchoolOperation schoolOpr = new()
{
PersonInfo = personInfo
};
schoolOpr.GetInfo(5);
}
Console.ReadKey();
}
}
}
Phương pháp Method Injection
Method Injection hay còn gọi là Interface Injection, lúc này chúng ta sẽ tạo ra một Interface Injector (IInfoAccessDependency) có chứa một Inject Method, Method này có một tham số chính là tham chiếu đến phụ thuộc của chúng ta cần tiêm vào. Sau đó Interface Injector này sẽ được triển khai tại lớp Client (SchoolOperation) sau đó Method này làm nhiệm vụ nhận phụ thuộc từ lớp Injector thông qua tham số của nó. Kết quả cũng sẽ tương tự với hai phương pháp phía trên.
namespace TypeOfDI
{
public interface IPersonInfo
{
string GetName(int id);
DateOnly GetDOD(int id);
string GetRole(int id);
}
public interface IInfoAccessDependency
{
void SetDependency(IPersonInfo dependency);
}
public class StudentInfo : IPersonInfo
{
public string GetName(int id)
{
return "Mike";
}
public DateOnly GetDOD(int id)
{
return new DateOnly(2000, 12, 05);
}
public string GetRole(int id)
{
return "Student";
}
}
public class TeacherInfo : IPersonInfo
{
public DateOnly GetDOD(int id)
{
return new DateOnly(1988, 12, 05);
}
public string GetName(int id)
{
return "Jame Sean";
}
public string GetRole(int id)
{
return "Teacher";
}
}
public class SchoolOperation : IInfoAccessDependency
{
private IPersonInfo? _personInfo;
public void SetDependency(IPersonInfo dependency)
{
_personInfo = dependency;
}
public void GetInfo(int id)
{
var name = _personInfo?.GetName(id);
Console.WriteLine($"Person's name with ID {id}: " + name);
var dob = _personInfo?.GetDOD(id);
Console.WriteLine($"Person's Date of Birth with ID {id}: " + dob);
var role = _personInfo?.GetRole(id);
Console.WriteLine($"Person's role with ID {id}: " + role);
}
}
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Do you want to display student information (1) or teacher information (2)? ");
var userSelection = Console.ReadLine();
IPersonInfo? personInfo = null;
switch (userSelection)
{
case "1":
personInfo = new StudentInfo();
break;
case "2":
personInfo = new TeacherInfo();
break;
default: break;
}
if (personInfo != null)
{
SchoolOperation schoolOpr = new();
schoolOpr.SetDependency(personInfo);
schoolOpr.GetInfo(5);
}
Console.ReadKey();
}
}
}
Vậy là chúng ta đã đi qua khái niệm về DI và cách cách tiêm phụ thuộc vào một đối tượng, ở bài này chúng ta đã tự tạo được cơ chế DI cơ bản và điều này sẽ giúp chúng ta nắm rõ về nguyên lý của kỹ thuật này. Trên thực tế, khi bạn làm việc với các dự án lớn thì số lượng các lớp phụ thuộc (dependent classes) là rất nhiều và vòng đời sử dụng của chúng là khác nhau, do đó chúng ta không thể tự tạo và quản lý vòng đời của các lớp này một cách thủ công vì rất tốn thời gian, khi đó các DI Container sẽ giúp chúng ta trong việc quản lý này. Hãy cùng tham khảo cách dùng DI Container ở bài sau nhé.