Introduction Dependency Injection on Android with Dagger 2
Bài đăng này đã không được cập nhật trong 9 năm
Dependency Injection là gì?
Dependency trong java
Dependency là sự phụ thuộc, kết nối giữa các module với nhau (trong java là hai lớp).
Ví dụ:
public class Client {
private Service service;
public Client(){
service = new ServiceImpl();
}
public void doSomething(){
...
service.doSomethingElse();
...
}
}
Trong ví dụ trên, class Client
chứa một thuộc tính Service
được khởi tạo trong constructor của Client
. Method doSomething()
gọi tới method doSomethingElse()
của class Service
. Ở đây ta nói Client
phụ thuộc cứng (hard-code dependency) vào Service
. Điều này tạo ra khó khăn cho việc kiểm thử unit cũng như gây khó khăn cho bản thân code khi cần nâng cấp, sửa đổi hay bảo trì.
- Khó khăn trong kiểm thử unit: trong trường hợp muốn kiểm thử độc lập method
doSomething()
trong classClient
mà muốn bỏ qua methoddoSomethingElse()
củaService
ta cần làm gì? Chúng ta có thể tạo ra một mock object củaService
để tiến hành kiểm thử methoddoSomething()
nhưng điều gì xảy ra nếu unit test fails, ta cần biết chính xác method nào fails?doSomething()
haydoSomethingElse()
? - Khó khăn khi nâng cấp, sửa đổi, bảo trì: khi yêu cầu thay đổi, ta cần thay
ServiceImpl
bằngServiceImp1
, việc thay đổi này lại ảnh hưởng tớiClient
vì instance củaservice
được khởi tạo trongClient
. Cũng tương ứng khi đó ta cần test lại cả hai lớpServiceImpl1
lẫn lớpClient
trong khi lớpClient
gần như không có thay đổi gì. Hard-code giữa các lớp, tạo ra các instance bằng từ khóanew
nên hết sức hạn chế, hoặc thiết kế ứng dụng với ít module, class hơn. Nếu áp dụng nguyên tắc thiết kế SOLID thì tương ứng với nó, từ khóanew
làm tăng mối liên hệ giữa các module (cần tránh trong nguyên lý Open-Close) và việc giảm số lương module làm giảm tính độc lập của module đó (cần tránh trong nguyên lý SRP).
Vậy phải làm cách nào để giải quyết vấn đề này?
Dependency Injection
Nếu ta không tạo ra instance của một module trong một module khác thì ta cần cung cấp module này theo cách khác - thông qua constructor, setter hoặc interface.
Trở lại ví dụ trên:
public class Client {
private Service service;
public Client(Service service){
this.service = service;
}
}
Ở đây, service
không được khởi tạo bên trong Client
mà được truyền vào thông qua constructor của Client
bằng việc sử dụng một interface
hay abstract class
Service
thay vì một class ServiceImpl
hay ServiceImpl1
- đó cũng là tư tưởng chính của nguyên tắc Dependency Inversion.
Tương tự với 2 phương pháp sử dụng setter và interface.
public class Client {
private Service service;
public void setService(Service service) {
this.service = service;
}
}
public interface ServiceSetter {
public void setService(Service service);
}
public class Client implements ServiceSetter {
private Service service;
@Override
public void setService(Service service) {
this.service = service;
}
}
Dependency Injection là phương pháp đảo ngược sự phụ thuộc thông qua việc truyền (injection) đối tuợng của class này vào đối tượng của class khác thông qua constructor. Đối tượng được khởi tạo ở một nơi khác và được truyền vào như một thuộc tính của constructor khi khởi tạo các đối tượng hiện tại.
Nhưng ở đây lại phát sinh ra một vấn đề mới. Nếu không khởi tạo module bên trong một module khác thì vẫn cần có một nơi mà module đó được tạo ra, bên cạnh đó, việc truyền vào thông qua constructor, setter hay interface với số lượng thuộc tính lớn sẽ khiến code bẩn và khó đọc. Điều đó có thể được giải quyết thông qua việc sử dụng Dependency Injector.
Dependency Injector Ta có thể coi đây như một module khác trong ứng dụng chịu trách nhiệm cung cấp các instance cho các lớp, các module khác. Việc tạo ra các module này được tập trung tại một nơi, tại một điểm duy nhất trong ứng dụng giúp ta có thể kiểm soát được nó.
Bắt đầu từ Java 5, annotations được đưa vào sử dụng. Chính annotations đã mở ra cách thức triển khai cũng như sử dụng Dependency Injector dễ dàng và hiệu quả nhất.
DI trong Android với Dagger 2
Introducion Dagger 2
Dagger 2 là một dependency injector, khác với các dependency injector dành cho việc triển khai ứng dụng Enterprise như Spring IoC hay JavaEE CDI, Dagger được thiết kế cho các thiết bị low-end, nhỏ gọn nhưng vẫn đầy đủ tính năng.
Hầu hết các dependency injector sử dụng reflection để tạo ra và inject các module. Reflection nhanh và thích hợp cho các version Android cũ nhưng reflection gây ra khó khăn rất lớn trong việc debug hay tracking khi gặp lỗi. Thay bằng việc sử dụng reflection Dagger sử dụng một trình biên dịch trước (pre-compiler), trình biên dịch này tạo ra tất cả các lớp, các module cần thiết để làm việc. Dagger ít mạnh mẽ so với các dependency injector khác nhưng thay vào đó Dagger lại nhẹ nhàng và dễ dàng sử dụng cũng như gần như bỏ đi được điểm yếu của dependency injector là khả năng tracking bug.
Dagger 2 API
Dagger 2 sử dụng một số annotations:
- @Module: sử dụng cho những class cung cấp các method dependencies
- @Provides: sử dụng cho methods nằm trong @Module class
- @Inject: yêu cầu một dependency (constructor, thuộc tính, method)
- @Component: một interface là cầu nối giữa Module và Injection
Dagger 2 Workflow
Để implement ứng dụng sử dụng Dagger 2 cần thực hiện các bước sau đây:
- Xác định các đối tượng và các liên kết, phụ thuộc giữa chúng.
- Tạo class với @Module annotation, sử dụng @Provides annotation cho method trả về các đối tượng của lớp phụ thuộc.
- Yêu cầu, lấy về các đối tượng phụ thuộc sử dụng @Inject annotation.
- Tạo một interface sử dụng @Component annotation và thêm các class @Module được tạo trong bước 2.
- Tạo một đối tượng từ @Component interface để tạo ra các instance cho các lớp phụ thuộc.
Implementing Dagger 2
Trong ví dụ này, tôi đưa ra một module đơn giản, với 2 lớp Vehicle
và Motor
. Trong đó lớp Vehicle
có một thuộc tính là đối tượng của class Motor
. Dưới đây sẽ trình bày từng bước để triển khai module này với Dagger 2.
Bước 1: Xác định các đối tượng và sự phụ thuộc giữa chúng
package io.quannh.android.daggerexample.model;
public class Motor {
private int rpm;
public Motor(){
this.rpm = 0;
}
public int getRpm(){
return rpm;
}
public void accelerate(int value){
rpm = rpm + value;
}
public void brake(){
rpm = 0;
}
}
Class Motor
rất đơn giản với chỉ một thuộc tính round-per-minute và 2 method accelerate(int)
và brake()
.
Class Vehicle
cũng rất đơn giản với một thuộc tính motor
được inject thông qua constructor cùng 2 method increaseSpeed(int)
và stop()
.
Ở đây class Vehicle
là class phụ thuộc vào class Motor
.
package io.quannh.android.daggerexample.model;
public class Vehicle {
private Motor motor;
public Vehicle(Motor motor){
this.motor = motor;
}
public void increaseSpeed(int value){
motor.accelerate(value);
}
public void stop(){
motor.brake();
}
public int getSpeed(){
return motor.getRpm();
}
}
Bước 2: Tạo @Module class
Trong bước này ta cần tạo một module class, class này làm nhiệm vụ cung cấp các đối tượng của các lớp khác nhau.
package io.quannh.android.daggerexample.module;
@Module
public class VehicleModule {
@Provides @Singleton
Motor provideMotor(){
return new Motor();
}
@Provides @Singleton
Vehicle provideVehicle(){
return new Vehicle(new Motor());
}
}
Ở đây ta tạo ra hai method trả về hai đối tượng của 2 lớp Motor
và Vehicle
. Lưu ý ở đây là lớp VehicleModule
bắt buộc phải sử dụng @Module annotations và các method cung cấp các đối tượng cần sử dụng @Provides annotations.
Bước 3: Request Dependency
Sau khi tạo class module ta cần đặt các @Inject annotations để Dagger 2 biết tại đó cần tạo mới và inject chính xác các lơp phụ thuộc. Trong ví dụ này ta cần thêm @Inject vào constructor của class Vehicle.
@Inject
public Vehicle(Motor motor){
this.motor = motor;
}
Bước 4: Kết nối @Modules với @Inject
Trong bước này ta tạo ra một interface với @Component annotations. Interface này làm nhiệm vụ liên kết giữa @Module class và @Inject class bằng việc chỉ rõ @Module class với cú pháp: @Component(modules = {VehicleModule.class})
Trong interface này ta định nghĩa các method trả về các kiểu đối tượng khác nhau, Dagger 2 sẽ tự động tạo ra các đối tượng, các module và inject chúng khi cần thiết và hoàn toàn tự động.
package io.quannh.android.daggerexample.component;
@Singleton
@Component(modules = {VehicleModule.class})
public interface VehicleComponent {
Vehicle provideVehicle();
}
Bước 5: Sử dụng @Component Interface
Sau khi tạo @Component interface ta tạo một instace của interface này và sử dụng nó cho việc gọi các method cần thiết.
public class MainActivity extends ActionBarActivity {
Vehicle vehicle;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
VehicleComponent component = Dagger_VehicleComponent.builder().vehicleModule(new VehicleModule()).build();
vehicle = component.provideVehicle();
Toast.makeText(this, String.valueOf(vehicle.getSpeed()), Toast.LENGTH_SHORT).show();
}
}
Dagger 2 cho phép tạo mới component object bằng cú pháp: Dagger_<NameOfTheComponentInterface>
, ở đây sử dụng Dagger_VehicleComponent
. Sau đó ta có thể gọi method builder()
sử dụng để tạo ra các module bên trong component.
Sau khi build()
, ta có được một instance của interface component, từ lúc này ta sử dụng instance này để tạo ra các instance của các class và module khác.
Bằng cách này ta dễ dàng tách các lớp riêng rẽ khỏi nhau, sử dụng unit-test cho từng module, từng class, bỏ qua việc tạo ra các mock object. Đồng thơi, phương pháp này cũng giúp ta dễ dàng trong việc thay đổi cấu trúc hay tạo ra các implement khác nhau cho các module.
Tổng kết
Dependency injection là một mô hình mà sớm hay muộn sẽ được sử dụng trong các ứng dụng khi cấu trúc ứng dụng liên tục thay đổi, phát triển, cần tái cấu trúc. Với Dagger 2, ta có một thư viện dễ sử dụng để thực hiện điều đó.
All rights reserved