Trang chủ » Blog » Đảo ngược quyền điều khiển (Phần 1)

Đảo ngược quyền điều khiển (Phần 1)

bởi CodeGym | 06/12/2023 17:29 | Blog

Series Bài viết này đề cập tới khái niệm Đảo ngược quyền điều khiển (IoC – Inversion of Control), “Tiêm” sự phụ thuộc (DI – Dependency Injection) và những ứng dụng của nó trong Spring Framework. Trong phạm vi bài viết, mình sẽ không đi sâu vào những định nghĩa khô khan mà chỉ cố gắng giải thích những khái niệm này một cách đơn giản và dễ hiểu nhất có thể . Mình muốn mang đến cho các bạn độc giả một hướng tiếp cận rõ ràng và gần gũi cho những khái niệm khá trừu tượng này. 

Ở phần 1, mình sẽ đề cập tới Phụ thuộc“Tiêm sự phụ thuộc” vì đây là một trong nhiều cách để tạo ra sự Đảo ngược quyền điều khiển. Trong các phần sau mình sẽ viết về Đảo ngược Quyền điều khiển và triển khai của nó thông qua Spring Framework.

Phụ thuộc là gì? Như thế nào là hard-code?

Sự phụ thuộc cũng giống câu chuyện của cái Laptop. Để Laptop hoạt động được , cần có các phần cứng như Ram, Ổ cứng, CPU, GPU hoạt động bên trong nó. Nếu những thành phần này lỗi hay dừng hoạt động thì Laptop của chúng ta cũng theo đó mà ngừng hoạt động luôn. Do vậy, Laptop phải Phụ thuộc vào các phần cứng như Ram, CPU, … để hoạt động.

Tiếp theo, hãy theo dõi đoạn code sau đây:

import java.util.ArrayList;

public class PeopleManager {
   private ArrayList<People> peopleList;

   public PeopleManager() {
       peopleList = new ArrayList<>();
   }

   public ArrayList<People> findAll() {
       return peopleList;
   }

   public People findOne(int id) {
       return peopleList.get(id);
   }

   public void remove(int id) {
       peopleList.remove(id);
   }
}

Các phương thức của lớp PeopleManager phải dựa vào các phương thức của lớp ArrayList để thực thi. Do vậy, để sử dụng được các phương thức này, chúng ta cần phải tạo một đối tượng (một thể hiện) của lớp ArrayList để truy cập tới những phương thức của lớp này. Việc khởi tạo đối tượng ArrayList thể hiện ở hàm tạo của PeopleManager.

public PeopleManager() {
   peopleList = new ArrayList<>();
}

Có thể nói rằng, việc tạo một đối tượng PeopleManager đã gián tiếp tạo ra một đối tượng ArrayList. Hay nói thế này cũng đúng, một đối tượng PeopleManager luôn có một đối tượng ArrayList ở bên trong nó. Khái quát hơn, đối tượng PeopleManager phải Phụ thuộc vào đối tượng ArrayList để thực thi các phương thức của mình. Việc viết mã như vậy được coi là hard-code. Hiểu nôm na nghĩa là code cứng đối tượng ArrayList vào code của PeopleManager. 

Việc này tưởng chừng đơn giản và bình thường nhưng lại kéo theo nhiều điểm bất cập. Khi mã nguồn của lớp ArrayList thay đổi thì phương thức của PeopleManager cũng bị thay đổi theo. Hay vì một lý do gì đó, chúng ta phải thay ArrayList bằng Linked List thì buộc phải can thiệp trực tiếp vào mã nguồn của lớp PeopleManager để thực hiện sự thay đổi này. Với những ứng dụng nhỏ, sự thay đổi này không quá khó khăn nhưng khi ứng dụng của chúng ta phát triển và mở rộng, các lớp quan hệ và giao tiếp với nhau thì việc thay đổi này sẽ trở nên khó khăn và đôi khi là không thực hiện được.

Các bạn hãy tưởng tượng code của chúng ta là cái đèn, và trong code của chúng ta có một đoạn dây cắm để cắm vào ổ điện giúp đèn hoạt động. Khi các bạn hard-code, nghĩa là các bạn đang hàn thẳng dây cắm vào ổ điện, làm cho đèn không rút ra được. Ổ cắm, dây điện, và bóng đèn của bạn trở thành một khối thống nhất. Do vậy, để sửa được đèn, các bạn phải tắt cầu trì, ngắt nguồn điện, tháo tung ổ điện ra, tháo tung bóng đèn ra, vân vân và mây mây… Nói chung là để sửa được đèn các bạn phải làm 3.14 bước rất loằng ngoằng so với việc code ra một cái phích cắm để có thể rút đèn ra khỏi ổ điện khi cần.

“Tiêm” sự phụ thuộc là gì?

Đảo ngược quyền điều khiển

Để giải quyết những bất cập trên, mình sử dụng một kỹ thuật có tên là “Tiêm” sự phụ thuộc. “Tiêm” ở đây nghĩa là truyền vào các thành phần phụ thuộc cho đối tượng của bạn. Kỹ thuật này dựa trên tính chất đa hình và trừu tượng trong lập trình hướng đối tượng.

Xin đừng hiểu lầm kỹ thuật này là hủy bỏ sự phụ thuộc, nó chỉ làm thay đổi cách thức phụ thuộc của hai đối tượng chứ không hủy sự phụ thuộc của chúng.

Hãy theo dõi đoạn code dưới đây:

import java.util.List;

public class PeopleManager {
  private List<People> peopleList;

  public PeopleManager() {
  }

  public PeopleManager(List<People> peopleList) {
      this.peopleList = peopleList;
  }

  public List<People> findAll() {
      return peopleList;
  }

  public People findOne(int id) {
      return peopleList.get(id);
  }

  public void remove(int id) {
      peopleList.remove(id);
  }

  public void setPeopleList(List<People> peopleList) {
      this.peopleList = peopleList;
  }
}

Trong đoạn code trên, mình đã sửa hàm tạo sao cho mỗi khi khởi tạo đối tượng của PeopleManager, chúng ta phải truyền vào nó một đối tượng List, hay chúng ta phải truyền cho nó một thành phần phụ thuộc để đối tượng PeopleManager có thể thực thi các phương thức của mình. ở đây, mình cũng thay đổi lớp của thành phần phụ thuộc thành Interface List để có thể truyền vào Linked List, ArrayList hay bất kì lớp nào triển khai từ Interface List. Điều này thể hiện ở hàm tạo của lớp PeopleManager:

public PeopleManager(List<People> peopleList) {
  this.peopleList = peopleList;
}

Bây giờ, thay vì việc lớp PeopleManager chỉ phụ thuộc trực tiếp vào lớp ArrayList, Lớp PeopleManager của mình đã có thể phụ thuộc vào nhiều lớp hơn, miễn là nó triển khai Interface List. Việc mở rộng code, sửa chữa, bảo trì bây giờ đã trở nên dễ dàng hơn do mình không cần phải can thiệp vào thẳng mã nguồn của lớp PeopleManager. Mình cũng không cần lo lắng tới chuyện một lớp nào đó nằm ngoài phạm vi của PeopleManager có thể gây lỗi cho nó.

Bằng cách truyền phần phụ thuộc như thế này, mình đã tách biệt các phần phụ thuộc ra khỏi mã nguồn của PeopleManager mà vẫn giữ được logic và kết quả trả về của các phương thức trong lớp đó. Để sử dụng được các phương thức của lớp PeopleManager trên, mình chỉ cần tạo một đối tượng ArrayList và truyền vào hàm tạo như trong đoạn code dưới đây:

import java.util.ArrayList;
import java.util.List;

public class Main {
  public static void main(String[] args) {
      List<People> peopleList = new ArrayList<>();
      PeopleManager peopleManager = new PeopleManager(peopleList);
  }
}

Lợi ích của việc truyền sự phụ thuộc còn thể hiện ở việc Kiểm thử (test) ứng dụng của bạn. Ở cách làm trước đây, các thành phần phụ thuộc nằm trong chính mã nguồn của lớp sử dụng, do đó, để kiểm thử phần mềm, chúng ta phải kiểm thử cả những thành phần phụ thuộc nằm trong đó. Việc này nhiều lúc trở nên bất khả thi và vất vả cho những ứng dụng lớn. Khi áp dụng cơ chế “Tiêm” sự phụ thuộc, việc test trở nên đơn giản vì bạn chỉ cần kiểm thử những phương thức nằm trong lớp đó mà không cần bận tâm tới những thành phần phụ thuộc kia. Việc kiểm thử cũng trở nên đa dạng và bao quát hơn khi bạn có thể truyền vào từng thành phần phụ thuộc khác nhau để kiểm tra xem mã nguồn có hoạt động chính xác trong mọi trường hợp hay không.

Sử dụng kỹ thuật Dependency Injection cũng giúp ứng dụng của bạn thỏa mãn tiêu chí “D” – Dependency Inversion Principle – Đảo ngược sự phụ thuộc trong nguyên lý SOLID. Nguyên lý này như sau: 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.

Đảo ngược quyền điều khiển

Để triển khai kỹ thuật này, chúng ta có thể sử dụng các phương pháp như sau:

  1. Constructor Injection: Các dependency sẽ được “tiêm” vào thông qua constructor của lớp đó. Đây là cách mình sử dụng ở ví dụ trên
  2. Setter Injection: Các dependency sẽ được “tiêm” vào thông qua các hàm setter
  3. Interface Injection: Các dependency sẽ được “tiêm” thông qua việc triển khai một phương thức có tên là inject. Đây là cách rắc rối và ít được sử dụng
  4. Sử dụng @AutoWire trong Spring Framework: Cách này chỉ sử dụng được với Spring Framework. Các dependency sẽ được “tiêm” thông qua các Java Bean và các dependency này sẽ ở dạng singleton, tức chỉ có một thể hiện.

Tuy kỹ thuật này khá thần thánh và ảo diệu nhưng việc áp dụng nó đúng cách cũng là một vấn đề. Nếu áp dụng mù quáng, sẽ khiến vấn đề trở nên phức tạp và bị “over-engineer” một cách quá đáng. Với những dự án đơn giản, ứng dụng không lớn, việc áp dụng Dependency Injection sẽ làm phức tạp hóa vấn đề, khiến dự án bị kéo dài thời gian phát triển. Để làm rõ cho vấn đề này, mình xin quote lại một số điểm so sánh trên blog toidicodedao như sau:

Đảo ngược quyền điều khiển

Kết

Việc đảo ngược Quyền điều khiển có thể triển khai qua nhiều cách nhưng hay dùng nhất vẫn là “tiêm” sự phụ thuộc. Qua bài viết này, mình hy vọng đem đến một cách tiếp cận gần gũi và dễ hiểu hơn về kỹ thuật này qua những ví dụ đơn giản và trực tiếp. Trong bài viết sau, mình sẽ chia sẻ về Đảo ngược quyền điều khiển và triển khai của nó trong Spring Framework. Hẹn các bạn trong bài viết tới!

Tham khảo:

https://en.wikipedia.org/wiki/Hard_coding

https://en.wikipedia.org/wiki/Dependency_injection

https://viblo.asia/p/dependency-injection-la-gi-va-khi-nao-thi-nen-su-dung-no-LzD5d0d05jY

https://toidicodedao.com/2015/11/03/dependency-injection-va-inversion-of-control-phan-1-dinh-nghia/

https://www.youtube.com/watch?v=EPv9-cHEmQw&list=WL&index=5&t=0s

Author: Nguyễn Văn Đức

Đăng ký nhận bộ tài liệu học Java trên 2 trang giấy tại đây

Xem thêm: Java Coding Bootcamp là gì? Tổng quan về Java Coding Bootcamp

Tags:

0 Lời bình

Gửi Lời bình

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *

BÀI VIẾT LIÊN QUAN

BẠN MUỐN HỌC LẬP TRÌNH?

GỌI NGAY

098 953 44 58

Đăng ký tư vấn lộ trình học lập trình

Đăng ký tư vấn, định hướng lộ trình học và giải đáp các thắc mắc về ngành nghề – Miễn phí – Online.

6 + 1 =

TƯ VẤN VỀ LỘ TRÌNH HỌC NGHỀ LẬP TRÌNH TẠI CODEGYM
TƯ VẤN VỀ LỘ TRÌNH HỌC NGHỀ LẬP TRÌNH TẠI CODEGYM