Dependency injection với Dagger 2 - Giới thiệu về DI

Thời gian trước, tại Google I/O Extended ở Tech Space đã giới thiệu về dependency injection với Dagger 2.

Dependency injection

Dependency injection là tất cả việc tạo các đối tượng (object) và truyền chúng cho nơi cần sử dụng. Quan sát ví dụ: class UserManager với 2 phụ thuộc là UserStoreApiService. Không thực hiện Dependency injection sẽ như sau:

user_manager_no_di.png

Cả hai đối tượng UserStoreApiService được khởi tạo bên trong class UserManager:

    class UserManager {

        private ApiService apiService;
        private UserStore userStore;

        //No-args constructor. Dependencies are created inside.
        public UserManager() {
            this.apiService = new ApiSerivce();
            this.userStore = new UserStore();
        }

        void registerUser() {/*  */}

    }

    class RegisterActivity extends Activity {

        private UserManager userManager;

        @Override
        protected void onCreate(Bundle b) {
            super.onCreate(b);
            this.userManager = new UserManager();
        }

        public void onRegisterClick(View v) {
            userManager.registerUser();
        }
    }

Đoạn code trên có vấn đề gì? Hãy tưởng tượng rằng, bạn muốn thay đổi cài đặt của UserStore bằng cơ chế lưu trữ của SharedPreferences. Nó sẽ cần đối tượng Context để tạo một thể hiện vì vậy hàm khởi tạo của UserStore sẽ cần truyền thêm tham số. Có nghĩa là bên trong class UserManager sẽ phải cập nhật lại lời gọi tạo đối tượng UserStore. Khi đó, nếu có hàng chục class mà sử dụng UserStore sẽ đều phải cập nhật lại.

Bây giờ, quan sát class UserManager có thực hiện Dependency injection:

user_manager_di.png

Các phụ thuộc được tạo và cung cấp bên ngoài class:

    class UserManager {

        private ApiService apiService;
        private UserStore userStore;

        //Dependencies are passed as arguments
        public UserManager(ApiService apiService, UserStore userStore) {
            this.apiService = apiService;
            this.userStore = userStore;
        }

        void registerUser() {/*  */}

    }

    class RegisterActivity extends Activity {

        private UserManager userManager;

        @Override
        protected void onCreate(Bundle b) {
            super.onCreate(b);
            ApiService api = ApiService.getInstance();
            UserStore store = UserStore.getInstance();

            this.userManager = new UserManager(api, store);
        }

        public void onRegisterClick(View v) {
            userManager.registerUser();
        }

    }

Khi đó, trong vấn đề tương tự, chúng ta thay đổi cài đặt của một phụ thuộc sẽ không cần cập nhật lại code của class UserManager. Tất cả các phụ thuộc của nó được truyền vào từ bên ngoài thông qua tham số (paramater), vì vậy chỉ có phần code khởi tạo đối tượng UserStore là phải được cập nhật lại.

Vậy, những lợi thế của việc sử dụng dependency injection là gì?

Tách biệt khởi tạo/sử dụng

Chúng ta khởi tạo các thể nghiệm của class chỉ một lần - thường là ở nơi khác với nơi sử dụng đối tượng. Nhờ cách tiếp cận này mà code của chúng ta được module hoá hơn, tất cả các phụ thuộc có thể được thay thế một cách đơn giản mà không có tác động lên logic của ứng dụng. Chẳng hạn, bạn muốn thay đổi DatabaseUserStore để sử dụng SharedPrefsUserStore, bạn chỉ cần quan tâm đến giao tiếp API (giống như DatabaseUserStore) hoặc cài đặt cùng một interface.

Sử dụng cho Unit test

Các unit test giả định rằng các class được test trong sự cô lập hoàn toàn mà không hề biết về các sự phụ thuộc của nó. Chẳng hạn, sau đây là một unit test cho class UserManager:

    public class UserManagerTests {

        UserManager userManager;

        @Mock
        ApiService apiServiceMock;
        @Mock
        UserStore userStoreMock;

        @Before
        public void setUp() {
            MockitoAnnotations.initMocks(this);
            userManager = new UserManager(apiServiceMock, userStoreMock);
        }

        @After
        public void tearDown() {
        }

        @Test
        public void testSomething() {
            //Test our userManager here - all its dependencies are satisfied
        }
    }
Phát triển độc lập/ đồng thời

Nhờ có việc module hoá code (UserStore có thể được cài đặt độc lập với UserManager) mà việc chia code để các lập trình viên cùng phát triển là dễ dàng. Mọi người chỉ phải quan tâm đến interface của UserStore (đặc biệt là các phương thức public của UserStore sử dụng trong UserManager).

Dependency injection frameworks

Bên cạnh những thuận lợi thì mẫu dependency injection cũng có một số nhược điểm. Một trong số đó là lượng code boilerplate lớn hơn. Hãy tưởng tượng đơn giản, class LoginActivity được cài đặt với mô hình MVP. Class này có thể trông như sau:

login_activity_diagram.png

Phần code chỉ chịu trách nhiệm khởi tạo LoginActivityPresenter như sau:

    public class LoginActivity extends AppCompatActivity {

        LoginActivityPresenter presenter;

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);

            OkHttpClient okHttpClient = new OkHttpClient();
            RestAdapter.Builder builder = new RestAdapter.Builder();
            builder.setClient(new OkClient(okHttpClient));
            RestAdapter restAdapter = builder.build();
            ApiService apiService = restAdapter.create(ApiService.class);
            UserManager userManager = UserManager.getInstance(apiService);

            UserDataStore userDataStore = UserDataStore.getInstance(
                    getSharedPreferences("prefs", MODE_PRIVATE)
            );

            //Presenter is initialized here
            presenter = new LoginActivityPresenter(this, userManager, userDataStore);
        }
    }

Trông không được thân thiện. Và đây là vấn đề mà DI sẽ giải quyết. Phần code có sử dụng DI như sau:

    public class LoginActivity extends AppCompatActivity {

        @Inject
        LoginActivityPresenter presenter;

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);

            //Satisfy all dependencies requested by @Inject annotation
            getDependenciesGraph().inject(this);
        }
    }

Và như vậy, code sẽ đơn giản hơn rất nhiều.

Kết luận

Như vậy, chúng ta đã tìm hiểu thế nào là dependency, thế nào là dependency injection. Việc sử dụng DI với mô hình MVP làm cho code được module hoá nhiều hơn, tính tách biệt được nâng cao và dễ dàng trong việc maintain và mở rộng phần mềm.

Tham khảo

https://medium.com/azimolabs/dagger-2-on-production-reducing-methods-count-5a13ff671e30