Chèn phần phụ thuộc trong Android

Chèn phần phụ thuộc (DI) là một kỹ thuật được sử dụng rộng rãi trong lĩnh vực lập trình và phù hợp với việc phát triển Android. Bằng cách tuân thủ các nguyên tắc của ID, bạn đã đặt nền tảng cho việc xây dựng một cấu trúc ứng dụng tốt.

Việc triển khai tính năng chèn phần phụ thuộc mang lại cho bạn những lợi ích sau:

  • Tái sử dụng mã
  • Dễ dàng tái cấu trúc
  • Dễ dàng kiểm thử

Kiến thức cơ bản về tính năng chèn phần phụ thuộc

Trước khi đề cập cụ thể về tính năng chèn phần phụ thuộc trong Android, trang này sẽ cung cấp thông tin tổng quan chung hơn về cách hoạt động của tính năng chèn phần phụ thuộc.

Chèn phần phụ thuộc là gì?

Các lớp thường yêu cầu tham chiếu đến các lớp khác. Ví dụ như một lớp Car có thể cần tham chiếu đến một lớp Engine. Các lớp bắt buộc này được gọi là phần phụ thuộc, và trong ví dụ này, lớp Car phụ thuộc vào việc có một phiên bản của lớp Engine để chạy.

Có ba cách để lớp này lấy đối tượng cần thiết:

  1. Lớp này sẽ tạo phần phụ thuộc cần thiết. Trong ví dụ trên, Car sẽ tạo và chạy thực thể riêng của Engine.
  2. Lấy từ một nơi khác. Một số API của Android, chẳng hạn như phương thức getter ContextgetSystemService() sẽ hoạt động theo cách này.
  3. Cung cấp các phần phụ thuộc dưới dạng tham số. Ứng dụng có thể cung cấp các phần phụ thuộc này khi lớp được tạo hoặc truyền chúng vào các hàm cần từng phần phụ thuộc. Trong ví dụ trên, hàm dựng Car sẽ nhận được Engine dưới dạng một tham số.

Tùy chọn thứ ba là chèn phần phụ thuộc! Với cách tiếp cận này, bạn sẽ lấy các phần phụ thuộc của một lớp và cung cấp các phần phụ thuộc đó thay vì thực thể của lớp đó tự lấy các phần phụ thuộc.

Dưới đây là một ví dụ. Nếu không có tính năng chèn phần phụ thuộc, việc biểu thị một Car mà tạo ra phần phụ thuộc Engine riêng trong mã sẽ có dạng như sau:

Kotlin

class Car {

    private val engine = Engine()

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}

Java

class Car {

    private Engine engine = new Engine();

    public void start() {
        engine.start();
    }
}


class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.start();
    }
}
Lớp ô tô không có phần phụ thuộc chèn

Đây không phải là một ví dụ về tính năng chèn phần phụ thuộc, vì lớp Car đang tạo Engine của riêng nó. Đây có thể là một vấn đề vì:

  • CarEngine được ghép nối chặt chẽ – một bản sao của Car sử dụng một loại Engine và không thể dễ dàng sử dụng các lớp con hoặc phương thức triển khai thay thế. Nếu Car tạo Engine riêng, bạn sẽ phải tạo 2 loại Car thay vì chỉ sử dụng lại cùng một Car cho các công cụ thuộc loại GasElectric.

  • Phần phụ thuộc cứng trên Engine khiến việc kiểm thử trở nên khó khăn hơn. Car sử dụng một thực thể thực của Engine, do đó, bạn không thể dùng kiểm thử kép để sửa đổi Engine cho các trường hợp kiểm thử khác nhau.

Mã này sẽ trông như thế nào khi chèn phần phụ thuộc? Thay vì mỗi bản sao của Car dựng đối tượng Engine riêng khi khởi chạy, hệ thống sẽ nhận được một đối tượng Engine dưới dạng tham số trong hàm khởi tạo:

Kotlin

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val engine = Engine()
    val car = Car(engine)
    car.start()
}

Java

class Car {

    private final Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.start();
    }
}


class MyApp {
    public static void main(String[] args) {
        Engine engine = new Engine();
        Car car = new Car(engine);
        car.start();
    }
}
Lớp ô tô sử dụng tính năng chèn phần phụ thuộc

Hàm main sử dụng Car. Vì Car phụ thuộc vào Engine nên ứng dụng sẽ tạo một thực thể của Engine, sau đó sử dụng thành phần đó để tạo một thực thể của Car. Lợi ích của phương pháp dựa trên DI này là:

  • Tái sử dụng Car. Bạn có thể truyền nhiều cách triển khai khác nhau của Engine sang Car. Chẳng hạn bạn có thể xác định một lớp con mới của Engine có tên là ElectricEngine mà bạn muốn Car sử dụng. Nếu sử dụng DI, bạn chỉ cần truyền vào một thực thể của lớp con ElectricEngine đã được cập nhật, còn Car vẫn hoạt động mà không cần thay đổi thêm.

  • Dễ dàng kiểm thử Car. Bạn có thể truyền kiểm thử kép để kiểm thử các tình huống khác nhau. Chẳng hạn bạn có thể tạo một kiểm thử kép của Engine có tên là FakeEngine, đồng thời định cấu hình kiểm thử đó cho nhiều kiểm thử khác nhau.

Có hai cách chính để chèn phần phụ thuộc trong Android:

  • Chèn hàm dựng. Đây là cách đã mô tả ở trên. Bạn truyền các phần phụ thuộc của một lớp tới hàm khởi tạo của lớp đó.

  • Chèn trường (hoặc chèn phương thức setter). Một số lớp khung Android nhất định như các hoạt động và mảnh là do hệ thống tạo bản sao, nên không thể chèn hàm khởi tạo. Với tính năng chèn trường, các phần phụ thuộc sẽ được tạo thực thể sau khi lớp được tạo. Mã sẽ có dạng như sau:

Kotlin

class Car {
    lateinit var engine: Engine

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.engine = Engine()
    car.start()
}

Java

class Car {

    private Engine engine;

    public void setEngine(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.start();
    }
}

class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.setEngine(new Engine());
        car.start();
    }
}

Chèn phần phụ thuộc tự động

Trong ví dụ trước, bạn đã tự tạo, cung cấp và quản lý các phần phụ thuộc của các lớp khác nhau mà không cần dựa vào thư viện. Phương pháp này được gọi là chèn phần phụ thuộc bằng tay hoặc chèn phần phụ thuộc theo cách thủ công. Trong ví dụ Car, chỉ có một phần phụ thuộc, nhưng việc có nhiều phần phụ thuộc và lớp hơn có thể khiến quá trình chèn các phần phụ thuộc theo cách thủ công trở nên tẻ nhạt hơn. Việc chèn phần phụ thuộc theo cách thủ công cũng gây ra một số vấn đề:

  • Đối với các ứng dụng lớn, việc lấy tất cả phần phụ thuộc và kết nối chúng đúng cách có thể cần đến một lượng lớn các mã nguyên mẫu. Trong cấu trúc nhiều lớp, để tạo một đối tượng cho lớp trên cùng, bạn phải cung cấp mọi phần phụ thuộc của các lớp bên dưới đối tượng đó. Một ví dụ cụ thể là để tạo một chiếc ô tô thật, bạn có thể cần một động cơ, hộp số, khung gầm và các bộ phận khác; còn động cơ thì cần có các xi lanh cũng như bộ đánh lửa.

  • Khi không thể tạo các phần phụ thuộc trước khi truyền các phần phụ thuộc đó vào, ví dụ: khi sử dụng tính năng khởi chạy từng phần hoặc xác định phạm vi đối tượng cho các luồng của ứng dụng, bạn cần viết và duy trì một vùng chứa tuỳ chỉnh (hoặc biểu đồ của phần phụ thuộc) để quản lý vòng đời của các phần phụ thuộc trong bộ nhớ.

Có nhiều thư viện giải quyết vấn đề này bằng cách tự động hoá quy trình tạo và cung cấp các phần phụ thuộc. Chúng phù hợp với hai danh mục:

  • Giải pháp dựa trên phản ánh kết nối các phần phụ thuộc trong thời gian chạy.

  • Các giải pháp tĩnh tạo mã để kết nối các phần phụ thuộc tại thời gian biên dịch.

Dagger là một thư viện chèn phần phụ thuộc phổ biến cho Java, Kotlin và Android do Google duy trì. Dagger hỗ trợ việc sử dụng DI trong ứng dụng bằng cách tạo và quản lý biểu đồ phần phụ thuộc cho bạn. Công cụ này cung cấp các phần phụ thuộc hoàn toàn tĩnh và thời gian biên dịch để giải quyết nhiều vấn đề về phát triển và hiệu suất của các giải pháp dựa trên cơ chế phản chiếu như Guice.

Lựa chọn thay thế cho tính năng chèn phần phụ thuộc

Một giải pháp thay thế cho tính năng chèn phần phụ thuộc là sử dụng công cụ định vị dịch vụ. Mẫu thiết kế công cụ định vị dịch vụ cũng giúp cải thiện việc phân tách các lớp khỏi các phần phụ thuộc cụ thể. Bạn tạo một lớp được gọi là công cụ định vị dịch vụ, có tác dụng tạo và lưu trữ các phần phụ thuộc, sau đó cung cấp các phần phụ thuộc đó theo yêu cầu.

Kotlin

object ServiceLocator {
    fun getEngine(): Engine = Engine()
}

class Car {
    private val engine = ServiceLocator.getEngine()

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}

Java

class ServiceLocator {

    private static ServiceLocator instance = null;

    private ServiceLocator() {}

    public static ServiceLocator getInstance() {
        if (instance == null) {
            synchronized(ServiceLocator.class) {
                instance = new ServiceLocator();
            }
        }
        return instance;
    }

    public Engine getEngine() {
        return new Engine();
    }
}

class Car {

    private Engine engine = ServiceLocator.getInstance().getEngine();

    public void start() {
        engine.start();
    }
}

class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.start();
    }
}

Mẫu công cụ định vị dịch vụ khác với tính năng chèn phần phụ thuộc ở cách sử dụng các phần tử. Với mẫu công cụ định vị dịch vụ, các lớp có quyền kiểm soát và yêu cầu đối tượng được chèn vào; bằng cách chèn phần phụ thuộc, ứng dụng có quyền kiểm soát và chủ động chèn các đối tượng bắt buộc.

So với tính năng chèn phần phụ thuộc:

  • Việc thu thập các phần phụ thuộc theo yêu cầu của một công cụ định vị dịch vụ khiến mã kiểm thử khó hơn, vì tất cả các kiểm thử phải tương tác với cùng một công cụ định vị dịch vụ toàn cầu.

  • Các phần phụ thuộc được mã hoá trong quá trình triển khai lớp chứ không phải trong nền tảng API. Vì vậy, thật khó để biết được một lớp cần gì từ bên ngoài. Do đó, những thay đổi đối với Car hoặc phần phụ thuộc có sẵn trong công cụ định vị dịch vụ có thể dẫn đến lỗi thời gian chạy hoặc thất bại trong kiểm thử do không thực hiện được các tham chiếu.

  • Việc quản lý thời gian hoạt động của các đối tượng sẽ khó khăn hơn nếu bạn muốn mở rộng bất kỳ đối tượng nào khác ngoài thời gian hoạt động của toàn bộ ứng dụng.

Sử dụng Hilt trong Ứng dụng Android

Hilt là thư viện đề xuất của Jetpack dùng để chèn phần phụ thuộc trong Android. Hilt xác định một phương pháp chuẩn để thực hiện DI trong ứng dụng của bạn, bằng cách cung cấp các vùng chứa cho mọi lớp Android trong dự án, đồng thời tự động quản lý vòng đời của các vùng chứa đó cho bạn.

Hilt được xây dựng dựa trên thư viện DI phổ biến là Dagger để hưởng lợi từ độ chính xác của thời gian biên dịch, hiệu suất trong thời gian chạy, khả năng có thể mở rộng và Hỗ trợ Android Studio mà Dagger cung cấp.

Để tìm hiểu thêm về Hilt, vui lòng xem bài viết Chèn phần phụ thuộc bằng Hilt.

Kết luận

Tính năng chèn phần phụ thuộc cung cấp cho ứng dụng của bạn những ưu điểm sau:

  • Khả năng tái sử dụng các lớp và phân tách các phần phụ thuộc: Dễ dàng hoán đổi các nội dung triển khai của một phần phụ thuộc. Khả năng sử dụng lại mã được cải thiện do tính năng đảo quyền kiểm soát, ngoài ra các lớp không còn kiểm soát cách các phần phụ thuộc được tạo ra nữa, mà thay vào đó sẽ hoạt động với bất kỳ cấu hình nào.

  • Dễ dàng tái cấu trúc: Các phần phụ thuộc trở thành một phần có thể xác minh của giao diện API, dó đó bạn có thể kiểm tra các phần phụ thuộc đó tại thời điểm tạo đối tượng, hoặc tại thời gian biên dịch thay vì ẩn dưới dạng thông tin triển khai.

  • Dễ dàng kiểm thử: Một lớp không quản lý các phần phụ thuộc của lớp đó, vì vậy khi kiểm thử, bạn có thể truyền nhiều phương thức triển khai để kiểm thử mọi trường hợp khác nhau.

Để hiểu đầy đủ về lợi ích của tính năng chèn phần phụ thuộc, bạn nên thử tính năng này trong ứng dụng theo cách thủ công, như được trình bày trong bài viết Chèn phần phụ thuộc theo cách thủ công.

Tài nguyên khác

Để tìm hiểu thêm về tính năng chèn phần phụ thuộc, hãy xem thêm các tài nguyên sau đây.

Mẫu