DIP và dependency injection
Nội dung bài · 5 mục
- 1.Khái niệm
- 2.Ví dụ
- 3.Thử ngay
- 4.Lỗi hay gặp
- 5.Tóm tắt
OrderService tự tạo SqlOrderRepository bên trong. Muốn chạy test thì phải
có database thật, muốn đổi sang lưu file thì phải sửa OrderService. DIP là
nguyên tắc cuối của SOLID, còn dependency injection là cách làm phổ biến nhất
để đạt được nó.
Khái niệm
🔌 DIP (Dependency Inversion Principle): code nghiệp vụ không phụ thuộc trực tiếp vào class cụ thể lo database hay email, mà cả hai cùng phụ thuộc vào interface.
💉 Dependency injection (DI): class nhận những thứ nó cần qua constructor, thay vì tự new bên trong.
Ví dụ
var repo = new SqlOrderRepository();
var service = new OrderService(repo);
service.Place("Bút bi");
interface IOrderRepository
{
void Save(string item);
}
class SqlOrderRepository : IOrderRepository
{
public void Save(string item) =>
Console.WriteLine($"SQL: lưu {item}");
}
class OrderService
{
private readonly IOrderRepository _repo;
public OrderService(IOrderRepository repo)
{
_repo = repo;
}
public void Place(string item) => _repo.Save(item);
}OrderServicechỉ biếtIOrderRepository, không biết có SQL. Đó là DIP.OrderServicenhận repository qua constructor. Đó là DI.PlaceOrderở bài Interface đã làm đúng như vậy.- Nơi tạo
OrderServicequyết định dùng repository nào. - Trong ASP.NET Core, framework tự tạo và truyền các object này. Bạn chỉ khai báo interface nào ứng với class nào.
Thử ngay
Chép ví dụ trên vào Program.cs. Thêm class dưới đây vào cuối file, rồi đổi
new SqlOrderRepository() ở dòng đầu thành new FakeOrderRepository():
class FakeOrderRepository : IOrderRepository
{
public void Save(string item) =>
Console.WriteLine($"Giả lập: lưu {item}");
}Đoán trước khi chạy: chương trình in ra dòng nào?
Xem kết quả
Giả lập: lưu Bút biOrderService gọi Save của object được truyền vào, nên in dòng của
FakeOrderRepository. Viết test không cần database thật cũng theo cách này:
truyền vào một repository giả.
Lỗi hay gặp
Tự new bên trong class. OrderService bị khoá cứng vào SQL.
// SAI — không đổi được, không test được
class OrderService
{
private readonly SqlOrderRepository _repo =
new SqlOrderRepository();
public void Place(string item) => _repo.Save(item);
}// ĐÚNG — nhận interface qua constructor
class OrderService
{
private readonly IOrderRepository _repo;
public OrderService(IOrderRepository repo)
{
_repo = repo;
}
public void Place(string item) => _repo.Save(item);
}Nhận class cụ thể qua constructor. Có DI nhưng vẫn phụ thuộc vào SQL, nên chưa đạt DIP.
// SAI — vẫn chỉ nhận được SqlOrderRepository
class OrderService
{
private readonly SqlOrderRepository _repo;
public OrderService(SqlOrderRepository repo)
{
_repo = repo;
}
}// ĐÚNG — kiểu tham số là interface
class OrderService
{
private readonly IOrderRepository _repo;
public OrderService(IOrderRepository repo)
{
_repo = repo;
}
}Tóm tắt
- DIP: code nghiệp vụ phụ thuộc vào interface, không phụ thuộc vào class cụ thể.
- DI: nhận phụ thuộc qua constructor, không tự
newbên trong. - Nhờ DI, đổi cách làm hay viết test chỉ cần truyền object khác vào.
- Constructor nên nhận interface, không nhận class cụ thể.
Tự kiểm tra
0/3 câuclass ReportService { private readonly PdfExporter _pdf = new PdfExporter(); } Vấn đề chính là gì?
Constructor nào đúng tinh thần DIP?
Lợi ích lớn nhất của DI khi viết test là gì?