Kiến thức cơ bản về dependency injection
Thuộc phần Lập trình hướng đối tượng trong hành trình C# của Coddy. Bài học 46 trên 70.
Tiêm phụ thuộc (Dependency Injection, DI) là kỹ thuật trong đó một lớp nhận các phần phụ thuộc từ bên ngoài thay vì tự tạo chúng. Cách này giúp code linh hoạt hơn, dễ kiểm thử hơn và giảm sự phụ thuộc chặt chẽ giữa các thành phần.
Khi không có DI, một lớp tự tạo các phần phụ thuộc của mình, khiến việc thay đổi hoặc kiểm thử trở nên khó khăn:
public class OrderService
{
private EmailSender sender = new EmailSender(); // Phụ thuộc chặt chẽ
public void PlaceOrder() => sender.Send("Order placed");
}Với DI, các phụ thuộc được truyền vào qua hàm khởi tạo:
public interface IMessageSender
{
void Send(string message);
}
public class EmailSender : IMessageSender
{
public void Send(string message) => Console.WriteLine("Email: " + message);
}
public class OrderService
{
private IMessageSender sender;
public OrderService(IMessageSender sender)
{
this.sender = sender;
}
public void PlaceOrder() => sender.Send("Order placed");
}Giờ đây, OrderService không biết và cũng không cần quan tâm nó đang dùng email, SMS hay bộ gửi giả lập để kiểm thử. Phụ thuộc được "tiêm" vào khi tạo đối tượng:
IMessageSender emailSender = new EmailSender();
OrderService service = new OrderService(emailSender);
service.PlaceOrder(); // Email: Đã đặt hàngMẫu này tuân theo nguyên tắc phụ thuộc vào các khái niệm trừu tượng (giao diện) thay vì các cách triển khai cụ thể. Nhờ đó, bạn có thể thay thế cách triển khai mà không cần sửa lớp sử dụng chúng. Điều này rất cần thiết để kiểm thử đơn vị và duy trì kiến trúc linh hoạt.
Thử thách
DễHãy xây dựng một hệ thống ghi nhật ký để minh họa sức mạnh của việc tiêm phụ thuộc. Thay vì cố định cách ghi nhật ký, bạn sẽ tạo một lớp Logger linh hoạt, có thể hoạt động với mọi nơi ghi nhật ký, như bảng điều khiển, mô phỏng tệp hoặc bất cứ nơi nào khác, mà không cần biết chi tiết cụ thể.
Bạn sẽ sắp xếp code trong ba tệp:
ILogWriter.cs: Định nghĩa một giao diện có tênILogWritertrong không gian tênLogging. Giao diện này đại diện cho quy ước mà mọi nơi ghi nhật ký cần tuân theo và chỉ có một phương thứcWrite(string message), trả về một chuỗi mô tả nơi thông báo được ghi.LogWriters.cs: Tạo hai cách triển khai cụ thể củaILogWritertrong không gian tênLogging:ConsoleLogWriter, phương thứcWritecủa lớp này trả vềConsole: {message}FileLogWriter, lớp này nhận tên tệp qua hàm khởi tạo và phương thứcWritetrả vềFile[{filename}]: {message}
Logger.cs: Tạo một lớpLoggertrong không gian tênLogging, nhận phụ thuộc thông qua tiêm phụ thuộc vào hàm khởi tạo. LớpLoggercần:- Nhận một
ILogWriterqua hàm khởi tạo và lưu nó vào một trường riêng tư - Có phương thức
Log(string message)gọi phương thức của trình ghi nhật ký được tiêm vào và trả về kết quả
- Nhận một
Program.cs: Minh họa việc tiêm phụ thuộc bằng cách tạo hai trình ghi nhật ký khác nhau, một trình dùngConsoleLogWritervà một trình dùngFileLogWriter. Cho thấy cùng một lớpLoggerhoạt động khác nhau tùy thuộc vào phụ thuộc được tiêm vào.
Bạn sẽ nhận hai đầu vào:
- Tên tệp dành cho trình ghi nhật ký vào tệp
- Thông báo cần ghi
Tạo một Logger với ConsoleLogWriter rồi ghi thông báo. Sau đó, tạo một Logger khác với FileLogWriter (dùng tên tệp từ đầu vào) rồi ghi cùng thông báo. In mỗi kết quả trên một dòng riêng.
Ví dụ, nếu đầu vào là app.log và System started, đầu ra sẽ là:
Console: System started
File[app.log]: System startedHãy chú ý rằng lớp Logger không bao giờ thay đổi, nó hoạt động với mọi cách triển khai ILogWriter. Đây chính là bản chất của việc tiêm phụ thuộc: Logger phụ thuộc vào một lớp trừu tượng (giao diện), còn cách triển khai cụ thể được “tiêm” từ bên ngoài. Bạn có thể dễ dàng thêm DatabaseLogWriter hoặc CloudLogWriter sau này mà không cần sửa đổi lớp Logger!
Tự làm thử
using System;
using Logging;
class Program
{
public static void Main(string[] args)
{
// Đọc các đầu vào
string filename = Console.ReadLine();
string message = Console.ReadLine();
// TODO: Tạo một Logger với ConsoleLogWriter và ghi lại thông điệp
// In ra kết quả
// TODO: Tạo một Logger khác với FileLogWriter (sử dụng filename) và ghi lại cùng thông điệp
// In ra kết quả
}
}
Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.
Tất cả bài học trong Lập trình hướng đối tượng
1Các khái niệm cơ bản về lập trình hướng đối tượng
Tệp bên ngoàiKhông gian tên và chỉ thịGiới thiệu về lớp và đối tượngTừ khóa thisPhương thức và tham sốTrường và thuộc tínhHàm khởi tạoKhởi tạo đối tượngÔn tập: Máy tính đơn giản4Kế thừa
Cú pháp kế thừa cơ bản (:)Từ khóa 'base'Từ khóa virtual và overrideLớp sealedLớp cơ sở 'object'Ôn tập: hệ phân cấp nhân viên7Tính năng nâng cao
Nạp chồng toán tửBộ lập chỉ mục (this[])Ghi đè ToString()Phương thức mở rộngÔn tập: Danh sách tùy chỉnh10Các mẫu thiết kế phần 1
Giới thiệu về mẫu thiết kếSingleton an toàn với luồngMẫu FactoryMẫu Observer (sự kiện)Mẫu Strategy2Thuộc tính và thành viên tĩnh
Thuộc tính tự độngThuộc tính chỉ đọc hoặc chỉ ghiTrường và phương thức tĩnhLớp tĩnhThành viên có thân biểu thức5Đa hình và interface
Đa hình lúc biên dịch và lúc chạyInterface và lớp trừu tượngNhiều interfaceInterface tường minhUpcasting và downcastingÔn tập: bộ tính toán hình học8Các khái niệm OOP nâng cao
Ưu tiên composition hơn kế thừaGenerics (lớp và phương thức)Delegate và sự kiệnAttribute và reflectionGiao diện IDisposable và câu lệnh usingKiến thức cơ bản về dependency injection11Các mẫu thiết kế phần 2
Mẫu lệnhMẫu bộ chuyển đổiMẫu trang tríMẫu phương thức khuôn mẫuMẫu trạng tháiMẫu tổ hợpTự luyện tập: Trình biên dịch C# online