Dependency Injection và Inversion of Control – Phần 1: Định nghĩa

Series bài viết Dependency Injection và Inversion of Control gồm 3 phần:

  1. Định nghĩa
  2. Áp dụng DI vào code
  3. Viết DI Container. Áp dụng DI vào ASP.NET MVC

Hôm trước, có vài bạn nhờ mình giải thích các khái niệm Dependency Injection, Inversion of Control. Vốn lười, mình định google một bài viết bằng tiếng Việt để quăng cho các bạn ấy. Ngặc một nỗi là mình chả tìm được bài nào cụ thể rõ ràng về Dependency Injection, chỉ có hướng dẫn sử dụng Unity, StructureMap. Một số bài viết hay thì lại toàn bằng tiếng Anh.

Mình cũng thấy vài bạn đã đi làm 1, 2 năm mà vẫn còn “ngáo ngơ” về DI, IoC, chỉ biết sử dụng nhưng không hiểu rõ bản chất của nó. Do đó, mình viết bài này nhằm giải thích một cách đơn giản dễ hiểu về Dependency Injection. Các bạn junior nên đọc thử, vì DI được áp dụng rất nhiều trong các ứng dụng doanh nghiệp, rất hay gặp khi đi làm/đi phỏng vấn. Pattern này được dùng trong cả C#, Java và các ngôn ngữ khác nên các bạn cứ đọc thoải mái nhé.

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

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é.

Nhắc lại kiến thức

Trước khi bắt đầu với Dependency Injection, các bạn hãy đọc lại bài viết về SOLID principles, những nguyên lý thiết kế và viết code. Nguyên lý cuối cùng trong SOLID chính là Dependency Inversion:

1. Các module cấp cao không nên phụ thuộc vào các modules 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.)

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 Dependendy Inversion principle, các module cùng 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.

Định nghĩa và khái niệm DI

Hiện nay, các lập trình viên hay lẫn lộn giữa các khái niệm Dependency Inversion, Inversion of Control (IoC), Dependency Injection (DI). Ba khái niệm này tương tự nhau nhưng không hoàn toàn giống nhau.

ioc-and-mapper-in-c-6-638

Sự khác biệt giữa 3 khái niệm trên:

  • Dependency Inversion: Đây là một nguyên lý để thiết kế và viết code.
  • Inversion of Control: Đây là một design pattern được tạo ra để code có thể tuân thủ nguyên lý Dependency Inversion. Có nhiều cách hiện thực pattern này: ServiceLocator, Event, Delegate, … Dependency Injection là một trong các cách đó.
  • Dependency Injection: Đây là một cách để hiện thực Inversion of Control Pattern (Có thể coi nó là một design pattern riêng cũng được). Các module phụ thuộc (dependency) sẽ được inject vào module cấp cao.

Khi nói tới DI, tức là nói tới Depedency Injection. Hiện nay, một số DI container như Unity, StructureMap v…v, hỗ trợ chúng ta trong việc cài đặt và áp dụng Dependency Injection vào code (Sẽ nói ở bài sau), tuy nhiên vẫn có thể gọi chúng là IoC Container, ý nghĩa tương tự nhau.

Có thể hiểu Dependency Injection một cách đơn giản như sau:

  1. Các module không giao tiếp trực tiếp với nhau, mà thông qua interface. Module cấp thấp sẽ implement interface, module cấp cao sẽ gọi module cấp thấp thông qua interface.
    1. Ví dụ: Để giao tiếp với database, ta có interface IDatabase, các module cấp thấp là XMLDatabase, SQLDatabase. Module cấp cao là CustomerBusiness sẽ chỉ sử dụng interface IDatabase.
  2. Việc khởi tạo các module cấp thấp sẽ do DI Container thực hiện. Ví dụ: Trong module CustomerBusiness, ta sẽ không khởi tạo IDatabase db = new XMLDatabase(), việc này sẽ do DI Container thực hiện. Module CustomerBusiness sẽ không biết gì về module XMLDatabase hay SQLDatabase.
  3. Việc Module nào gắn với interface nào sẽ được config trong code hoặc trong file XML.
  4. DI được dùng để làm giảm sự phụ thuộc giữa các module, dễ dàng hơn trong việc thay đổi module, bảo trì code và testing.

Các dạng DI

Có 3 dạng Dependency Injection:

  1. Constructor Injection: Các dependency sẽ được container truyền vào (inject vào) 1 class thông qua constructor của class đó. Đây là cách thông dụng nhất.
  2. Setter Injection: Các dependency sẽ được truyền vào 1 class thông qua các hàm Setter.
  3. Interface Injection: Class cần inject sẽ implement 1 interface. Interface này chứa 1 hàm tên Inject. Container sẽ injection dependency vào 1 class thông qua việc gọi hàm Inject của interface đó. Đây là cách rườm rà và ít được sử dụng nhất.

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

Ưu điểm và khuyết điểm của DI

Dĩ nhiên, DI không phải vạn năng, nó cũng có những ưu điểm và khuyết điểm, do đó không phải project nào cũng nên áp dụng DI. Với những dự án lớn, code nhiều, DI là thứ rất cần thiết để đảm bảo code dễ bảo trì, dễ thay đổi. Vì vậy, bản thân các framework nổi tiếng như Spring, Struts2, ASP.NET MVC, … đều hỗ trợ hoặc tích hợp sẵn DI. ASP.NET MVC từ bản 5 trở xuống cho phép ta sử dụng DI container từ thư viện, từ bản 6 thì tích hợp sẵn DI luôn, không cần phải thêm thư viện gì.

Ư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
  • Dễ dàng thấy quan hệ giữa các module (Vì các dependecy đều được inject vào constructor)
  • 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
  • Các object được khởi tạo toàn bộ ngay từ đầu, có thể làm giảm performance
  • Làm tăng độ phức tạp của code

Bài viết khá nặng về lý thuyết nên nếu bạn vẫn chưa mường tượng được sẽ áp dụng DI như thế nào vào code cũng đừng lo. Ở phần 2 mình sẽ bổ sung code minh họa, các bạn đọc xong quay lại đọc bài này sẽ dễ “thông” hơn. Nhớ đón xem nhé.

30s quảng cáo

book.jpg

Đây là một bài viết được trích dẫn từ cuốn sách “Code dạo kí sự – Lập trình viên đâu phải chỉ biết code” do mình viết. Quyển sách bao gồm những kĩ năng từ mềm đến cứng mà mỗi developer phải có, đảm bảo sẽ rất có ích cho các bạn sinh viên hoặc lập trình viên đã đi làm. Các bạn xem thông tin và đặt mua sách tại đây nhé: Sách Code Dạo Ký Sự.

Bonus: Một slide về Dependency Injection mình từng làm cho buổi thuyết trình trên công ty

32 thoughts on “Dependency Injection và Inversion of Control – Phần 1: Định nghĩa”

  1. Bài viết rất chi tiết và dễ hiểu, mà hìnt như ngoài Dependency Inj còn Dependency Absorption

    Like

      1. Do hồi lâu lúc tìm hiểu về pattern DI rồi sau đó mới chọn framework để xài thì mình có đọc mấy bài của tụi Ấn nó viết, có ông kia chia thành 4 cái mà lâu ùi k đọc lại nên k rõ đúng hem nữa.

        Like

  2. Cho tôi hỏi cái, Module cấp thấp cấp cao ở đây làm sao mình biết được cái đó là cao hay thấp và dựa trên cái hình thứ 2 của bài thì module nào là cao module nào là thấp?

    Like

    1. Cái nào được thằng khác gọi thì là cấp thấp, gọi thằng khác thì là cấp cao. Giống như gái gọi là cấp thấp còn người gọi là cấp cao ấy :v

      Like

    1. Mình đã dạo một vòng các trang (cả tiếng Anh và tiếng Việt) và cũng thấy có các blog nhầm lẫn, thậm chí là lẫn lộn khái niệm DIP với khái niệm của IoC. Thực sự là cũng dễ bị nhầm. Chính vì vậy mà đọc đến comment của bạn mình phải login vào vì nhìn thấy một quan điểm giống mình :v

      Like

  3. Chắc chắn rằng Service Locator không phải là một cách để thực thi IoC. Thực tế, Service Locator sử dụng phương pháp đi ngược với nguyên lý của IoC.
    IoC là một khái niệm rộng, và một khía cạnh của khái niệm này được áp dụng để support Dependency Inversion. DI là tên gọi của khía cạnh này. Nói cách khác, DI là một phần của IoC, chứ không phải là “cách để hiện thực IoC”.
    Ngoài ra, Event đúng là một cách để thực thi IoC, nhưng không phải là IoC ở khía cạnh support Dependency Inversion, vậy nên không thể đặt cùng tầng với DI.
    Nói tóm lại, cách hiểu IoC của tác giả là không ổn. IoC không phải là một design pattern. Nó là một nguyên lý (Principle) để xây dựng Framework. IoC không phải sinh ra để thực thi duy nhất Dependency Inversion, IoC support nhiều thứ, và một trong số đó là Dependency Inversion. .

    Liked by 1 person

      1. Cảm ơn bạn Hoang Vy Duong.
        Cảm ơn bạn Hoàng, đọc xong bài này kiếm docs đọc lại mới được. Trước giờ vẫn còn lơ mơ phần này.

        Like

  4. lôi bài cũ ra đọc mới thấy sai chính tả nè Hoàng: rất cần thiền => rất cần thiết

    Like

  5. hi Bạn, Trong phần khuyết điểm thấy bạn có nói là nó khởi tạo all object nên giảm performance, mình nghĩ cái này không đúng thực chất khi init web nó chỉ register thôi chưa init object, chỉ khi nào sử dụng object của class đó, nó mơi init object.
    Bạn nghĩ thế nào?

    Like

    1. Em cũng đang định góp ý cái này!

      Thường thì tuỳ thuộc vào loại dự án và có thể do trình độ người cài đặt!!
      Đa phần quá trình khởi chạy của DI không hẳn sẽ init all object như trong phần khuyết điểm nói đến. Hầu hết sẽ thông qua register để kiểm soát các chức năng, cài đặt ban đầu ( cấu hình chưa chắc đã cứng trong code ), khi nó lời gọi ( thường qua getter kết hợp caster ) thì DI Container mới bắt đầu khởi tạo theo cấu hình đăng ký trong register.

      Ngay cả Config cũng có thể là 1 injecter và chỉ động đến khi có lời gọi tới.

      Ngoài ra, hầu hết các dự án dùng DI thì em kết hợp cả 2 phương pháp là init qua constructor và setter, từ đó xác định được loại injecter là là cần khởi chạy ban đầu, loại nào chỉ cần chạy khi được gọi là giải phóng nếu cần thiết!!

      Khó khăn lớn nhất là khi người mới vào làm bằng DI sẽ không hiểu rõ ràng tư tưởng, khiến quá trình làm DI vẫn bị nhập nhằng và các injecter bị ràng buộc mà không thoát hẳn ra theo tư tưởng của DI.

      Like

  6. “Module cấp thấp sẽ implement interface, module cấp cao sẽ gọi module cấp thấp.” chỗ này phải là “Module cấp thấp sẽ implement interface, module cấp cao sẽ gọi interface.” chứ nhỉ? Hoàng viết nhầm thì phải.

    Like

  7. Em mới biết đến kênh Youtube của anh gần đây, và lần đầu tiên vào blog của anh, các bài viết của anh rất hữu ích và dễ hiểu. Cảm ơn anh rất nhiều ạ.
    Hy vọng một ngày không xa em cũng đủ giỏi để chia sẽ những kiến thức của mình, đóng góp cho cộng đồng giống như anh 😀 Thích anh quá hà =))

    Like

  8. Chủ blog có lẽ nên viết lại bài để đính chính lại khái niệm về IoC. Thực tế thì IoC và DIP đều là 2 principle. Chúng mặc dù liên quan đến nhau vì đều focus vào việc decouple các objects nhưng lại khác nhau hoàn toàn. Khi nói về IoC thì mình đang đề cập về việc “Cái nào” sẽ khởi tạo lệnh gọi giữa reusable module và specific-purpose code. Còn khi đề cập về DIP thì mình đang nói về mức độ (level) của sự trừu tượng. Nghĩa là các object depend on implementation hay là abstraction?

    Like

Leave a comment