Design Pattern, OO Design Principles, Tản mạn

Tản mạn về Dependency Injection

Tản mạn về Dependency Injection

Như thường lệ, blog của mình sẽ rất ít các định nghĩa, rất nhiều code, hình ảnh minh họa và những câu nói dí dỏm. OK, hãy bắt đầu với định nghĩa về Dependency

Dependency

Khi trong class A có sự tồn tại của lớp B, dùng lớp B để làm 1 công việc gì đó, ta nói lớp A đang phụ thuộc vào lớp B. Ví dụ: CustomerRepository.java

package com.edward.tutorial.di.repository;

import java.util.Collection;

import com.edward.tutorial.di.model.Customer;

public class CustomerRepository {
	public Customer findOne(String customerId) {
		// Code stuff
	}

	public Collection<Customer> findAll() {
		// Code stuff
	}

	public void create(Customer customer) {
		// Code stuff
	}

	public boolean update(Customer customer) {
		// Code stuff
	}

	public void delete(Customer customer) {
		// Code stuff
	}
}

CustomerService.java

package com.edward.tutorial.di.service;

import java.util.Collection;

import com.edward.tutorial.di.model.Customer;
import com.edward.tutorial.di.repository.CustomerRepository;

public class CustomerService {
	
	CustomerRepository customerRepository;
	
	Collection<Customer> findAll() {
		return customerRepository.findAll();
	}

	Customer create(Customer customer) {
		customerRepository.create(customer);
		return customer;
	}
	
	Customer update(Customer customer) {
		customerRepository.update(customer);
		return customer;
	}

	void delete(String customerId) {
		Customer customer = findOne(customerId);
		customerRepository.delete(customer);
	}
	
	public Customer findOne(String customerId) {
		return customerRepository.findOne(customerId);
	}
}

Như ví dụ ở trên, lớp CustomerService đang phụ thuộc vào lớp CustomerRepository, vì CustomerService đang sử dụng 1 instance của CustomerRepository để thực hiện việc lưu trữ dữ liệu xuống database, cụ thể ở đây là customerRepository. Dependency1 Vậy: customerRepository được khởi tạo ở đâu? Vì nếu không được khởi tạo, thì khi sử dụng chúng ta sẽ nhận được NullPointerException. Câu trả lời là: để sử dụng được đối tượng customerRepository trong CustomerService, chúng ta sẽ có 2 cách:

  1. Khởi tạo đối tượng customerRepository trong lớp CustomerService.
  2. Truyền (Inject) đối tượng customerRepository đã được khởi tạo sẵn từ bên ngoài vào lớp CustomerService.

Dependency2

Injection

CHÚNG TA VỪA NHẮC ĐẾN INJECT. ĐÚNG VẬY, KHI LÀM THEO CÁCH 2, NGHĨA LÀ CHÚNG TA ĐANG THỰC HIỆN DEPENDENCY INJECTION. MỘT LƯU Ý KHÁC: Nếu chỉ muốn hiểu Dependency Injection là gì thì xin chúc mừng, chúng ta đã hoàn thành mục tiêu ở đây. Tuy nhiên chắc sẽ chẳng ai muốn dừng lại tại đây. Cho nên chúng ta sẽ đi tiếp phần demo và những chủ đề liên quan đến Dependency Injection Tiếp tục với việc demo cho 2 cách trên. Dependency3 Vậy các bạn sẽ tự hỏi: cách nào là tốt nhất, lúc nào nên dùng cách nào? Câu trả lời vẫn như mọi khi: không cách nào là hoàn hảo.

Tightly coupling và Loosely coupling

Trước khi đi vào phân tích ưu nhược của các cách làm, chúng ta cần làm rõ 2 khái niệm: tightly coupling và loosely coupling.

Tightly coupling:

Ở cách 1, khi khởi tạo đối tượng customerRepository ngay trong lớp CustomerService, ta nói lớp CustomerService đang phụ thuộc hoàn toàn vào lớp CustomerRepository. Trong suốt quá trình chương trình được thực thi (runtime), lớp CustomerService sẽ chỉ làm việc với duy nhất thể hiện của lớp CustomerRepository. Ở cách 2, mặc dù chúng ta không khởi tạo trực tiếp đối tượng customerRepository trong lớp CustomerService mà inject từ ngoài vào, nhưng lớp CustomerService vẫn chỉ làm việc được đối tượng customerRepository, do đó, cách 2 vẫn là Tightly coupling. Vậy, tightly coupling là việc một lớp TRỰC TIẾP sử dụng (phụ thuộc) trực tiếp vào thể hiện cụ thể của 1 lớp khác. TightlyCoupling

Loosely coupling:

Trái với tightly coupling, dễ dàng suy ra loosely coupling là việc một lớp KHÔNG TRỰC TIẾP sử dụng (phụ thuộc) vào thể hiện cụ thể của bất cứ 1 lớp cụ thể nào. Cho đến bây giờ vẫn chưa có 1 ví dụ nào để cho thấy loosely coupling như thế nào. Trong Java (hoặc các ngôn ngữ lập trình hướng đối tượng khác), để đạt được loosely coupling trong thiết kế, chúng ta cần sử dụng khái niệm “đa hình” (polymorphism). Tính đa hình trong lập trình hướng đối tượng là việc định nghĩa những đối tượng trừu tượng (abstraction) như abstract class hoặc interface. Những đối tượng abstraction này khai báo hoặc đưa ra quy ước (contract) và cho phép nhiều lớp đối tượng cụ thể kế thừa và định nghĩa lại hành vi cho riêng chúng.

Ví dụ về tính đa hình

Một tổ chức tiêu chuẩn ISO nào đó đưa ra định nghĩa về chiếc điện thoại: điện thoại là vật có khả năng nghe và nhận cuộc gọi. Lúc này ISO chưa hề cho ra đời 1 chiếc điện thoại nào có thể cầm nắm được, túm lại, nó chỉ là định nghĩa, để khi có người đang cầm trên tay 1 chiếc điện thoại thực sự thì họ có thể gọi hoặc nhận cuộc gọi. Sau khi đã có định nghĩa thế nào là điện thoại, các nhà sản xuất điện thoại hàng đầu thế giới mới bắt tay vào việc sản xuất ra những chiếc điện thoại theo tiêu chuẩn đã đặt ra bởi ISO. Hiện tại có 2 nhà sản xuất là Nokia và Samsung. Mục đích của họ là phải sản xuất ra những chiếc điện thoại có thể nghe và gọi được theo tiêu chuẩn ISO. Khi những chiếc điện thoại được tung ra thị trường, người sử dụng sau đó mua về và thực hiện chức năng nghe gọi mà họ đã từng được nghe ISO công bố trước đó. Tuy nhiên khi thực hiện cuộc gọi từ điện thoại Nokia thì sẽ nghe câu “Hello Microsoft” đầu tiên, còn với Samsung sẽ là câu “Lee Yong-dae” chẳng hạn. (I love Badminton). Có một nhà sản xuất nghiệp dư (giấu tên) cũng tập tành sản xuất điện thoại nhưng không tuân theo tiêu chuẩn của ISO, vậy nên khi cho ra thị trường, người ta không thể thực hiện nghe gọi, mà chỉ có thể… nghe nhạc. Vậy nên có thể gọi đây là cái máy nghe nhạc (tuân theo tiêu chuẩn của một tổ chức nào đó dành cho máy nghe nhạc), chứ không phải cái điện thoại. Phần mô tả cho đa hình đến đây là hết. Cười chút đã. =)) Do vậy, một lớp được gợi là loosely couling khi nó chỉ phụ thuộc vào các đối tượng high level (Abstract class hoặc Interface) thay vì phụ thuộc vào các lớp đối tượng cụ thể (concrete class). (Giống như việc một người tuyên bố, tui biết sử dụng tất cả điện thoại, miễn là nó có thể gọi và nghe, chứ không riêng gì cái điện thoại mà cứ phát ra câu “Lee Yong-dae”.) Ví dụ với bài Điện thoại theo chuẩn ISO ở trên

package com.edward.tutorial.di.phone;

public abstract class Phone {
	public abstract void call(String number);
	public abstract void answer(String number);
}

package com.edward.tutorial.di.phone;

public class NokiaPhone extends Phone {

	@Override
	public void call(String number) {
		System.out.println("Microsoft... " + number);
	}

	@Override
	public void answer(String number) {
		System.out.println(number + " Buy me...");
	}
}

package com.edward.tutorial.di.phone;

public class SamsungPhone extends Phone {

	@Override
	public void call(String number) {
		System.out.println("Kpop Kpop Kpop... " + number);
	}

	@Override
	public void answer(String number) {
		System.out.println(number + " I wanna cry...");
	}
}

package com.edward.tutorial.di.phone;

public class User {
	Phone phone;
	
	public User(Phone phone) {
		this.phone = phone;
	}
	
	public void bringMePhone(Phone phone) {
		this.phone = phone;
	}
	
	public void call(String number) {
		phone.call(number);
	}
	
	public void answer(String number) {
		phone.answer(number);
	}
}

package com.edward.tutorial.di.phone;

public class Main {

	public static void main(String[] args) {
		User bill = new User(new SamsungPhone());
		bill.call("0123");
		bill.bringMePhone(new NokiaPhone());
		bill.answer("0124");
	}
}

Quay lại ví dụ của chúng ta Extract CustomerRepository class thành CustomerRepository interface

package com.edward.tutorial.di.repository;

import java.util.Collection;

import com.edward.tutorial.di.model.Customer;

public interface CustomerRepository {

	public abstract Customer findOne(String customerId);

	public abstract Collection<Customer> findAll();

	public abstract void create(Customer customer);

	public abstract boolean update(Customer customer);

	public abstract void delete(Customer customer);

}

CustomerRepositoryImpl sẽ implement CustomerRepository interface

package com.edward.tutorial.di.repository;

import java.util.Collection;

import com.edward.tutorial.di.model.Customer;

public class CustomerRepositoryImpl implements CustomerRepository {
	@Override
	public Customer findOne(String customerId) {
		// Code stuff
	}

	@Override
	public Collection<Customer> findAll() {
		// Code stuff
	}

	@Override
	public void create(Customer customer) {
		// Code stuff
	}

	@Override
	public boolean update(Customer customer) {
		// Code stuff
	}

	@Override
	public void delete(Customer customer) {
		// Code stuff
	}
}

Và inject CustomerRepository interface thay vì CustomerRepositoryImpl

package com.edward.tutorial.di.service;

import java.util.Collection;

import com.edward.tutorial.di.model.Customer;
import com.edward.tutorial.di.repository.CustomerRepository;

public class CustomerService {
	
	CustomerRepository customerRepository;
	
	public CustomerService(CustomerRepository customerRepository) {
		this.customerRepository = customerRepository;
	}
	
	Collection<Customer> findAll() {
		return customerRepository.findAll();
	}

	Customer create(Customer customer) {
		customerRepository.create(customer);
		return customer;
	}
	
	Customer update(Customer customer) {
		customerRepository.update(customer);
		return customer;
	}

	void delete(String customerId) {
		Customer customer = findOne(customerId);
		customerRepository.delete(customer);
	}
	
	public Customer findOne(String customerId) {
		return customerRepository.findOne(customerId);
	}
}

LooselyCoupling

Nên design theo Tightly coupling hay Loosely coupling

Câu trả lời là Loosely coupling. Vì sao? Đọc tiếp…

Loosely coupling = Dependency Injection + Abstraction

Dependency Injection không giúp chúng ta cải thiện tính flexibility trong thiết kế. Tuy nhiên khi chúng ta kết hợp với Abstraction bằng cách inject các đối tượng abtract, thì chúng ta đã có một bản thiết kế loosely coupling và flexibility.

Dependency inversion principle

Dependency inversion principle là 1 trong 5 nguyên lý thiết kế hướng đối tượng (S.O.L.I.D). Xem thêm tại (https://edwardthienhoang.wordpress.com/2013/11/27/the-dependency-inversion-principle/) Nói đơn giản, Dependency inversion principle giúp chúng ta có được 1 thiết kế loosely coupling Dependency inversion principle = Loosely coupling = Dependency Injection + Abstraction

Inversion of Control (IoC)

Nếu như Dependency inversion là principle thì Inversion of Control là pattern tuân theo principle này. Trong Java, IoC pattern xuất hiện khá nhiều. Lấy 1 ví dụ rất quen thuộc, đó là hàm public static void main(String[] args)

package com.edward.tutorial.di;

public class Main {

    public static void main(String[] args) {
        System.out.println("Developer's dream starts from here");
        doMyWish();
    }

    private static void doMyWish() {
        System.out.println("I wish this app would be the most favourite one in the world");
    }
}

Tất cả các lập trình viên Java đều quá quen thuộc với hàm main này – entry point của 1 chương trình Java truyền thống. Lần này chúng ta sẽ không bàn đến cú pháp, mà bàn đến một việc mà được cho là “chẳng ai quan tâm là mấy”. Câu hỏi là: hàm doMyWish() được gọi bởi hàm main(), vậy hàm main() được gọi bởi hàm nào? Câu trả lời là: Java sẽ gọi hàm main() của chúng ta. Vậy làm thế nào mà Java (chính xác hơn là javac.exe hay đại loại là một cái cao siêu gì đó) có thể gọi được hàm main của chúng ta?

Hollywood principle: “Don’t call us, we’ll call you.”

Dịch nôm na: “Đừng có gọi cho tui, để lại số điện thoại, tui sẽ gọi bạn.” Ngôi sao mà, có quyền chảnh. Nếu nói Java là 1 ngôi sao Hollywood, và hàm main là 1 ai đó, thì việc chúng ta cần làm là nói với ngôi sao đó: “Đây! Địa chỉ của tui đây”. Cung cấp cho Java đường dẫn tới class com.edward.tutorial.di.Main chứa hàm main. Việc còn lại cứ để Java lo, Java sẽ gọi hàm main trong class com.edward.tutorial.di.Main và “Developer’s dream” will be started. LƯU Ý: Hollywood principle chỉ là cách gọi khác của Dependency inversion principle. Thêm 1 ví dụ dễ thấy.

JButton helloButton = new JButton("Hello");
helloButton.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("I would say Hello to everyone!");
    }
});

Chúng ta không hề gọi hàm actionPerformed(ActionEvent e), mà chỉ khai báo như vậy, và nói: “Hey! Java Swing or EventDispatchThread or something like that, call me when someone click the helloButton”. Một lần nữa, Java sẽ lo việc đó.

Inversion of Control Container (IoC Container)

Chúng ta đã thấy sự tuyệt vời với những đoạn code ở trên, Inversion of Control Container chính là những ngôi sao Hollywood thực thụ, họ định nghĩa ra Hollywood principle, công việc của lập trình viên là: viết ra những đoạn code và nhờ IoC Container đâu là điểm entry point Trong Eclipse, chúng ta dễ dàng config entry point cho 1 chương trình Java tại: Config_Main

Inversion of Control Container Framework (IoC Container Framework)

Trong ứng dụng Java truyền thống, IoC Container có thể được hiểu như những ví dụ ở trên. Tuy nhiên, trong những ứng dụng Java Enterprise, IoC Container mới có nhiều đất diễn, có khá nhiều Framework hỗ trợ IoC Container tạm gọi là IoC Container Framework như: JEE Framework hoặc Spring Framework. Nơi mà những khái niệm về: Dependency Injection, Inversion of Control hay Inversion of Control Container sẽ được nhắc đến như là những Core Feature. Tất cả đều hướng đến một mục đích duy nhất: tạo ra ứng dụng loosely coupling, flexibility cũng như giúp lập trình viên tập trung 99% vào công việc cho business flow.

Loosely coupling và Testing

Một lợi ích không thể nhắc đến khi thiết kế chương trình theo loosely coupling style chính là Testing. Thay vì phải inject những đối tượng thực vào trong SUT (System under test, hay nôm na là class cần test), chúng ta có thể inject các stub đã implement interface vào trong SUT. Chi tiết sẽ được bàn trong bài viết về UnitTesting. 🙂

Learn from Question&Answer

Có một bạn hỏi như sau:
Nói về chủ đề này, với DI thì ok. Bài viết này của anh khá sâu, tuy nhiên phần ioc lại chưa đc rõ rằng lắm. Vẫn còn khá chung chung. A có thể giải thích cho em vài vấn đề sau. Giữa DI và ioc
1. Thằng nào là con thằng nào?. Quan điểm con của nhau có đúng ko?.
2. Theo em đc biết, DI là 1 design pattern xây dựng lên để đáp ứng cho tính lỏng lẻo, giảm sự liên kết phụ thuộc và khi đó IoC chính là thằng thực hiện theo cái DI đó, nó sẽ sử dụng BeanFactory và ApplicationContext để thao tác lấy thông tin từ XML xử lý, tạo đối tượng. Khi đó, với quan điểm của em thì em nghĩ chẳng thằng nào là con thằng nào cả.

Answer:
Một câu hỏi khá hay. Trong cái thắc mắc của em đã trả lời luôn rồi.
Đúng như em nói: DI là 1 Design Pattern “để đáp ứng cho tính lỏng lẻo, giảm sự liên kết phụ thuộc”. IoC thì rộng hơn, và thường đi liền với Framework. Framework sẽ invert và control phần xương sống của application được develop bên trên, và “chừa”, “cung cấp” 1 số “cổng” để developer có thể inject phần implement của mình vào application. (Inject ở đây có 2 cách hiểu như 2 ví dụ dưới đây.

1. BeanFactory và ApplicationContext: em đang nói đến Spring Framework, vậy đó, Spring cho phép em inject các implementation của mình vào application qua ApplicationContext và 1 số Bean XML. Rõ ràng, DI được dùng để làm chuyện này thông qua @Autowired hay @Inject (CDI)

2. Tuy nhiên, trong bài viết của anh còn đề cập đến một chuyện đơn giản hơn. Đó là Java Swing Framework. Swing cho phép chúng ta định nghĩa ra “những action to be peformed” khi 1 event cụ thể được fired. Còn việc fire đó sẽ do Swing chứ ko phải do mình. DI không được dùng trong đây, nghĩa là Swing không cần đến pattern DI mà vẫn làm được việc Invertion of Control.
Từ 2 ví dụ trên có thể cho em hiểu Invertion Of Control là gì và mối quan hệ với DI rồi nhé.

Kết thúc bài tại đây, hi vọng trong giới hạn hiểu biết cùng với cách giải thích của mình sẽ giúp các bạn có thêm 1 cái nhìn khác về các khái niệm: Dependency Injection, Dependency Inversion Principle, IoC.. Trong những bài sau, chúng ta sẽ bàn luận về IoC Container trong Spring Framework. Happy learning

NGUỒN: HTTP://EDWARDTHIENHOANG.WORDPRESS.COM/ EMAIL: EDWARDTHIENHOANG@GMAIL.COM

Advertisements

2 thoughts on “Tản mạn về Dependency Injection”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s