12/08/2018, 10:31

Introduction Dependency Injection on Android with Dagger 2

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 ( ) ; } ...

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 class Client mà muốn bỏ qua method doSomethingElse() của Service ta cần làm gì? Chúng ta có thể tạo ra một mock object của Service để tiến hành kiểm thử method doSomething() 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() hay doSomethingElse()?
  • 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ằng ServiceImp1, việc thay đổi này lại ảnh hưởng tới Client vì instance của service được khởi tạo trong Client. Cũng tương ứng khi đó ta cần test lại cả hai lớp ServiceImpl1 lẫn lớp Client trong khi lớp Client 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óa new 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óa new 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:

  1. Xác định các đối tượng và các liên kết, phụ thuộc giữa chúng.
  2. 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.
  3. Yêu cầu, lấy về các đối tượng phụ thuộc sử dụng @Inject annotation.
  4. Tạo một interface sử dụng @Component annotation và thêm các class @Module được tạo trong bước 2.
  5. 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 đó.

0