Menu
CoddyTech

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àng

Mẫ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.

challenge icon

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ên ILogWriter trong không gian tên Logging. 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ức Write(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ủa ILogWriter trong không gian tên Logging:
    • ConsoleLogWriter, phương thức Write củ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ức Write trả về File[{filename}]: {message}
  • Logger.cs: Tạo một lớp Logger trong không gian tên Logging, nhận phụ thuộc thông qua tiêm phụ thuộc vào hàm khởi tạo. Lớp Logger cần:
    • Nhận một ILogWriter qua 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ả
  • 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ùng ConsoleLogWriter và một trình dùng FileLogWriter. Cho thấy cùng một lớp Logger hoạ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 started

Hã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ả
    }
}
quiz iconTự kiểm tra

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

Tự luyện tập: Trình biên dịch C# online