Series SOLID cho thanh niên code CỨNG: Dependency Inversion Principle

Giới thiệu

Chào mừng các bạn đến với bài viết cuối cùng trong series SOLID. Ở bài viết này, mình sẽ nói về Dependency Inversion Principle – Nguyên lý Đảo Ngược Dependency.

  1. Single Responsibility Principle
  2. Open/Closed Principle
  3. Liskov Substitution Principle
  4. Interface Segregation Principle
  5. Dependency Inversion Principle

Nội dung nguyên lý

1. Các module cấp cao không nên phụ thuộc vào các module cấp thấp. Cả 2 nên phụ thuộc vào abstraction.
2. Interface (abstraction) không nên phụ thuộc vào chi tiết, mà ngược lại. (Các class giao tiếp với nhau thông qua interface, không phải thông qua implementation.)

dependency_inversion_principle_thumb

Giải thích nguyên lý

Trong bài, mình hay dùng từ module. Trong thực tế, module này có thể là 1 project, 1 file dll, hoặc một service. Để dễ hiểu, chỉ trong bài viết này, các bạn hãy xem mỗi module là một class nhé.

Với cách code thông thường, các module cấp cao sẽ gọi các module cấp thấp. Module cấp cao sẽ phụ thuộc và module cấp thấp, điều đó tạo ra các dependency. Khi module cấp thấp thay đổi, module cấp cao phải thay đổi theo. Một thay đổi sẽ kéo theo hàng loạt thay đổi, giảm khả năng bảo trì của code.

ioc-and-mapper-in-c-4-638

Nếu tuân theo DIP, các module cấp thấp lẫn cấp cao đều phụ thuộc vào 1 interface không đổi. Ta có thể dễ dàng thay thế, sửa đổi module cấp thấp mà không ảnh hưởng gì tới module cấp cao.

Để dễ hiểu, bạn hãy nhìn vào cái mấy cái đèn điện trong nhà mình. Ở đây, module cấp cao chính là ổ điện, interface chính là đuôi đèn tròn, 2 module cấp thấp là bóng đèn tròn bóng đèn huỳnh quang.

Hai module này đều kế thừa interface đuổi tròn, Ta có thể dễ dàng thay đổi 2 loại bóng vì module cấp cao (ổ điện) chỉ quan tâm tới interface (đuôi tròn), không quan tâm tới implementation (bóng đèn tròn hay huỳnh quang).

abcd

Trong code cũng vậy, khi áp dụng DIP, các module liên kết với nhau thông qua interface. Để kết nối tới database, ta chỉ cần gọi hàm Get, Save … của interface IDataAccess. Khi thay database, ta chỉ cần thay implementation của interface này.

dependency-inversion_principles
Abstraction ở đây chính là interface

Ví dụ minh họa

Code khi chưa áp dụng DIP:


// Cart là module cấp cao
public class Cart
{
public void Checkout(int orderId, int userId)
{
// Database, Logger, EmailSender là module cấp thấy
Database db = new Database();
db.Save(orderId);
Logger log = new Logger();
log.LogInfo("Order has been checkout");
EmailSender es = new EmailSender();
es.SendEmail(userId);
}
}

view raw

Cart.cs

hosted with ❤ by GitHub

Code sau khi đã thiết kế lại, áp dụng DIP:


// Interface
public interface IDatabase
{
void Save(int orderId);
}
public interface ILogger
{
void LogInfo(string info);
}
public interface IEmailSender
{
void SendEmail(int userId);
}
// Các Module implement các Interface
public class Logger : ILogger
{
public void LogInfo(string info) {}
}
public class Database : IDatabase
{
public void Save(int orderId) {}
}
public class EmailSender : IEmailSender
{
public void SendEmail(int userId) {}
}
// Hàm checkout mới sẽ như sau
public void Checkout(int orderId, int userId)
{
// Nếu muốn thay đổi database, logger ta chỉ cần thay dòng code dưới
// Các Module XMLDatabase, SQLDatabase phải implement IDatabase
//IDatabase db = new XMLDatabase();
//IDatebase db = new SQLDatabase();
IDatabase db = new Database();
db.Save(orderId);
ILogger log = new Logger();
log.LogInfo("Order has been checkout");
IEmailSender es = new EmailSender();
es.SendEmail(userId);
}

view raw

interface.cs

hosted with ❤ by GitHub

Trong thực tế, người ta thường áp dụng pattern Dependency Injection để đảm bảo nguyên lý DIP trong code. Mình đã giải thích kĩ về code trong bài viết này nên không nhắc lại nữa, các bạn vào xem nhé: https://toidicodedao.com/2015/11/10/dependency-injection-va-inversion-of-control-phan-2-ap-dung-di-vao-code/

ioc-and-mapper-in-c-8-638

Lưu ý và kết luận

DIP được áp dụng nhiều nhất trong code, nhưng nó cũng gây ra nhiều điều tranh cãi. Bên cạnh một số ưu điểm, DIP cũng đi kèm một số khuyết điểm sau:

ƯU ĐIỂM KHUYẾT ĐIỂM
  • Giảm sự kết dính giữa các module
  • Code dễ bảo trì, dễ thay thế module
  • Rất dễ test và viết Unit Test
  • Khái niệm DI khá “khó tiêu”, các developer mới sẽ gặp khó khăn khi học
  • Sử dụng interface nên đôi khi sẽ khó debug, do không biết chính xác module nào được gọi
  • Làm tăng độ phức tạp của code

Mình từng gặp một số dự án mà mỗi interface chỉ có 1 class implement nó, cũng không có Unit Test gì cả. Trong trường hợp này, DIP không mang lại nhiều lợi ích, áp dụng DI chỉ khiến cho code dài dòng, khó debug hơn.

ioc-and-mapper-in-c-1-638

Một số tài liệu để tham khảo thêm:

Lời kết

Chúc mừng các bạn đã cùng mình hoàn thành series SOLID cho thanh niên code cứng này. Hi vọng qua series, các bạn có thể thu được những kiến thức hữu ích và áp dụng chúng vào việc thiết kế/ viết code.

Như mình đã nói ở bài đầu tiên,  các nguyên lý này chỉ là hướng dẫn, giúp cho code của bạn tốt hơn, sạch hơn, dễ bảo trì hơn. Tuy nhiên, “không có bữa ăn trưa nào miễn phí”. Áp dụng các nguyên lý này vào code có thể giúp bạn cải thiện được chất lượng code, nhưng cũng có thể làm nó rườm rà, dài hơn, khó quản lý hơn.

Nhắc lại một điều mình đã nói trong bài design pattern: Để thành một developer giỏi, ta nên biết các nguyên lý SOLID, các design patterns. Tuy nhiên, không phải cứ cứng nhắc áp dụng nhiều nguyên lý và design pattern vào code thì code sẽ tốt hơn. Một người developer giỏi sẽ hiểu rõ những trade-off của chúng và chỉ áp dụng một cách hợp lý để giải quyết vấn đề. Hãy nhớ, trong design, tất cả đều là đánh đổi nhé!

1
Đừng để code của bạn biến thành thế này nhé

11 thoughts on “Series SOLID cho thanh niên code CỨNG: Dependency Inversion Principle”

  1. “Mình từng gặp một số dự án mà mỗi interface chỉ có 1 class implement nó, cũng không có Unit Test gì cả.”
    —> Móa nhột quá nha honey! Đúng là có 1 class nhưng hên là có Unit Test gỡ gạc lại.

    Liked by 1 person

  2. Lúc trước đi phỏng vấn gặp 1 ông nói:
    – IDatabase db = new Database();
    – db.Save(orderId);
    là vi phạm nguyên tắc DI Injection. Hắn cho rằng dùng từ khóa new là vi phạm nguyên tắc -> làm mình hoang mang quá. Bác chủ thớt cho cái ví dụ sâu hơn để tìm hiểu thêm đi.

    Liked by 1 person

    1. đúng rồi đó bạn mình cũng mới học spring thấy các đối tượng hay các beans đều được tạo ra và quản lý bởi container hết , mình chỉ khai báo bằng file xml và anotation thôi ,

      Like

    2. Denpendency Injection là 1 design pattern để thực hiện nguyên lí Dependency Inversion.
      Cách khai báo trên về nguyên tắc sẽ không đúng với Denpendency Injection, nhiệm vụ khởi tạo các đối tượng sẽ là nhiện vụ của DI container. Trong các Class ta chỉ inject các Interfaces(các module con) vào thôi.

      Like

  3. Theo mình, để thể hiện về DIP triệt để hơn, hàm checkout chỉ cần biết đến database là 1 service chuyên thực thi các hàm lưu trữ, cập nhật dữ liệu, ko cần quan tâm đến việc khởi tạo nó như thế nào.
    Do vậy trong ví dụ code về DIP của bạn mình thấy có chỗ chưa ổn lắm:

    IDatabase db = new Database();
    db.Save(orderId);

    Một instance của db vẫn được khởi tạo bằng từ khóa new, như vậy nếu chúng ta thay đổi constructor của IDatabase, cho thêm tham số connectionString chẳng hạn, thì sự thay đổi từ module cấp thấp (Database) sẽ làm chúng ta phải sửa code ở module cấp cao (thêm biến connectionString vào hàm khởi tạo của database ở hàm checkout). Như vậy vẫn vi phạm DIP.

    Like

    1. Ở đây có thể dùng 1 service khác để inject object database vào, mình khai báo new để các bạn đọc dễ hiểu về DIP thôi. Trong bài viết mình có để link tới cách làm đúng là Dependency Injection nhé bạn. Thanks bạn đã góp ý ;).

      Like

  4. Đoạn code dưới so với trên thì tốt hơn chỗ nào, trong khi cả 2 trường hợp đều phải sửa code trong hàm checkout?

    Like

  5. Hi anh,
    em có một thắc mắc như vầy. Nếu 1 module cấp thấp inject 1 module cấp thấp khác thì có vi phạm nguyên tắc của DI ko a? Và nếu bỏ đi DiContainer (vì e thấy 1 số người khởi tạo các module trong module cấp cao hơn). Vậy 2 trường hợp trên có vi phạm nguyên tắc của DI ko a?
    thanks a

    Like

Leave a comment