Dependency injection với Dagger 2
Dependency injection (DI) là gì?, Dagger là gì? và làm sao mà chúng giúp chúng ta viết code một cách clean hơn và dễ dàng để test hơn?
DI là một kỹ thuật giúp việc viết unittest một cách dễ dàng hơn, không cần thay đổi code khi các thành phần nó phụ thuộc của nó thay đổi. Dagger là một trong những DI framework phổ biến nhất cho Java và Android.
Để giới thiệu Dagger 2 người đọc cần hiểu về DI nên trước hết chúng ta cùng tìm hiểu xem DI là gì?
Dependency injection là gì?
DI giúp class dễ dàng hơn để test và sử dụng lại. Để hiểu cách thực hiện DI chúng ta cùng xem ví dụ sau.
Ví dụ chúng ta thực hiện một service thực hiện việc in ra thông tin thời tiết, một cách hiện thực bình thường mà người viết thường thấy như sau:
WeatherReporter
cần hai đối tượng là LocationProvider
cung cấp địa điểm và WeatherService
cung cấp thông tin thời tiết tại địa điểm cung cấp.
Trong lập trình OOP một thiết kế tốt là một class chỉ có một trách nhiệm rõ ràng và phụ thuộc vào các đối tượng khác để hoàn thành công việc. Các đối tượng đó gọi là dependencies. Trước khi class có thể chạy thì các đối tượng mà nó phụ thuộc phải được cung cấp trước bằng một cách nào đó. Trong ví dụ trên ta có thể thấy là các đối tượng được khởi tạo ngày trong class WeatherReporter
.
Việc khởi tạo các dependencies trong contructor
đối với các service nhỏ thì không có vấn đề gì nhiều. Nhưng khi các service lớn dần lên, nó sẽ làm cho các class trở nên không linh động, phải sửa đổi. Ví dụ, yêu cầu thay đổi sử dụng một weather service khác như GoogleWeatherService
, lúc này sẽ phải vào từng class có sử dụng WeatherService
để đổi thành GoogleWeatherService
.
Thứ hai là liên quan đến phần unittest, thật không dễ dàng để test khi khi khởi tạo class WeatherReporter
thì hai đối tượng kia được khởi tạo cùng và trở thành một phần trong quá trình test, sẽ thế nào nếu dependencies có các dependencies khác của chính nó, và dependencies thực hiện các thao tác liên quan đến network,... Có thể thấy cách hiện thực như trên sẽ không đảm bảo được các yếu tố của unittest.
Để giải quyết vấn đề trên, class không nên chỉ quan tâm đến việc nó có thể chỉ cần thực hiện được công việc của nó, mà còn là quan tâm đến cách nó lấy những đối tượng mà nó phụ thuộc để thực hiện xong công việc. Nếu thay vì tự khởi tạo các dependencies class nhận các dependencies từ bên ngoài truyền vào thì vấn đề trên sẽ được giải quyết.
Cách hiện thực trên được xem là dependency injnection , tất cả dependencies mà class cần được bên ngoài cung cấp chứ không phải tự đi kiếm hoặc khởi tạo.
Như cách làm trên WeatherService
, LocationProvider
được chuyển thành interface
, class WeatherReporter
nhận các dependencies từ bên ngoài truyền vào, các dependencies có thể là bất cứ implement nào thực hiện interface trên, khi có thay đổi về ví dụ LocationProvider
từ google sang yahoo thì chỉ cần thay đổi 1 dòng code duy nhất bên ngoài thay vì phải sửa từng class sử dụng dependency location.
Nếu project chỉ có từng đó đối tượng, rất ít dependencies thì DI như trên chẳng có vấn đề gì, nhưng đi xa hơn một tí. Ví dụ, LocationProvider
có dependencies của nó là GPSProvider
để định vị trí hiện tại, ExploreService
để cung cấp các địa điểm hấp dẫn xung quanh, WeatherService
cần WebSocket
để giao tiếp với đài khí tượng thủy văn, lúc này hàm main sẽ như thế này:
Tưởng tượng rằng WebSocket
, GPSProvider
,... có dependencies riêng của nó, và cách khởi tạo không chỉ đơn giản là một dòng new
duy nhất mà phải đọc file, khởi tạo theo logic,... lúc này hàm main sẽ là một đống hỗn độn.
Vậy Dagger 2 giúp chúng ta như thế nào?
Dagger 2 là một open source giúp chúng ta tự động tạo các mã code dựa vào các @annotation
khi compilation. Trong khi một số framework khác, ví dụ như Spring thì DI được hiện thực dựa vào reflection khiến nó chậm hơn khi runtime.
Với Dagger 2, ví dụ phía trên sẽ đơn giản trở thành như thế này:
Các dependencies được tự động khởi tạo mà chúng ta không cần phải tự viết từng dòng code và đặt vào các contructor. Class DaggerAppComponent được tạo ra bởi Dagger, khi gọi hàm getWeatherReporter
dagger sẽ tự tìm các dependencies, khởi tạo chúng, truyền vào class cần và trả về WeatherReporter
.
Tất nhiên để dagger làm được việc đó cần một số bước:
- maven
2. Inject: Add annotation @inject
vào đầu contructor của tất cả class mà yêu cầu dependency hoặc là dependency của class khác để dagger có thể biết tới nó.
3. Module: build một object nào đó là dependency của một class khác.
Ví dụ, WeatherReport
cần WeatherService
và hiện tại chúng ta đang sử dụng service của google, hãy khởi tạo một module của google.
Module cần được annotation bằng @Module
, các method của nó cung cấp các instance của dependency được đặt annotation @Provides
, tên các hàm này không quan trọng, dagger chỉ quan tâm @Provides
để biết là nó cung cấp instance của dependency và loại đối tượng mà nó trả về là gì.
Có thể thấy là hàm provideWeatherService
nhận WebSocket
là dependency của GoogleWeatherService
, dagger sẽ tự tìm kiếm các module để lấy dependency WebSocket
và truyền vào.
annotation @Singleton
ở đây có ý nghĩa là class WebSocket
sẽ được khởi tạo singleton.
4. Component: Cuối cùng cần tạo một interface với annotation @Component
, chứa hàm trả về method mà chúng ta cần. Chúng ta không cần phải tạo từng hàm cho từng dependency mà chỉ là đối tượng cần để chạy, như ví dụ trên là WeatherReporter, còn những dependency của nó sẽ được dagger tự khởi tạo.
Dagger tìm kiếm các dependencies mà WeatherReporter
cần, cũng như các dependencies mà các dependencies cần theo tuần tự ở các modules, khởi tạo chúng, tạo nên cấu trúc dependencies và trả về đối tượng duy nhất của hàm getWeatherReporter
.
Lưu ý rằng cùng một đối tượng ví dụ WeatherService
không được có cùng lúc hai @Provides
trong các module {GoogleModule.class, CommonModule.class} truyền vào Component nếu không Dagger sẽ không biết lấy đối tượng nào.
Nếu như trong các module có một đối tượng được @Provides với scope @Singleton thì Component cũng phải cùng scope là Singleton. Như ví dụ trên, CommonModule cung cấp WebSocket với scope Singleton nên AppComponent cũng phải chung scope.
5. Custom và switch module.
Ví dụ rằng GoogleModule cung cấp WeatherService, để lấy được thông tin thời tiết nó cần WebSocket và một private_key
để giao tiếp với server Google, WebSocket là dependency được khởi tạo và cung cấp bởi dagger, còn private_key
được cung cấp trong config file.
Lúc này cần custom GoogleModule như sau:
Hàm main lúc này sẽ như sau, builder yêu cầu truyền google module vào vì google module được cung cấp cho component:
Còn trong trường hợp muốn chuyển đổi WeatherService
từ GoogleWeatherService
sang YahooWeatherService
thì sao?
Trước hết cần định nghĩa @Provides cho YahooWeatherService
, trong trường hợp này chúng ta tạo ra một module riêng cho yahoo.
Add YahooModule vào Component và remove GoogleModule, vì hai module này đều @Provide WeatherService, dagger không cho phép như vậy:
Lúc này như trên, hàm main sẽ như sau:
Kết bài
Bài viết đã giới thiệu tổng quan về DI và Dagger 2 đã giúp chúng ta hiện thực DI một cách dễ dàng như thế nào. Hi vọng bạn đọc sẽ có thể áp dụng Dagger 2 dễ dàng và thấy được lợi ích từ nó.