MVP là 1 trong những kiến trúc phổ biến nhất trong lập trình Android. Tuy có rất nhiều cách để implement MVP và mỗi người lại có 1 cách tiếp cận khác nhau, mục đích chính của cấu trúc này vẫn là việc tách rời UI layer và business layer, nhằm làm cho chúng ta có thể test được từng layer riêng vì giờ đây chúng đã không còn phụ thuộc vào nhau nữa.

Thời gian gần đây, tôi nhận thấy 1 xu hướng của nhiều lập trình viên là viết thêm 1 interface cho presenter, thậm chí cả sample cấu trúc của Google cũng làm như vậy. Chợt nhớ tới 1 bài viết mà tôi đã đọc cách đây khá lâu nên hôm nay muốn chia sẻ với các bạn.

Interfaces for presenters in MVP are a waste of time!

Đã được một khoảng thời gian từ lần cuối chúng tôi nói về MVP ở Karumi. Hôm nay, chủ đề thảo luận là việc tạo interface cho Presenter trong MVP có thật sự cần thiết không.

Dưới đây là giản đồ cấu trúc MVP:

Trong giản đồ này, Model có liên quan đến tất cả những đoạn code cần thiết để implement business logic. Presenter là class implement presentation logic và View là 1 interface được tạo ra để trừu tượng hóa đối tượng view thực (như Activity hoặc Fragment).

Tại sao phải implement view với 1 interface trong cấu trúc này? Bởi vì chúng ta muốn tách những chức năng chính ra khỏi view. Chúng ta muốn trừu tượng hóa những framework được sử dụng để viết presentation layer, bất kể chúng có dependency nào. Chúng ta muốn có thể dễ dàng thay đổi implementation của view nếu cần. Chúng ta muốn làm theo các nguyên tắc sử dụng dependency để cải thiện unit test. Hãy nhớ rằng để có thể tuân theo các nguyên tắc sử dụng dependency, những khái niệm bậc cao (high level concepts) như implementation của presenter không thể phụ thuộc vào những chi tiết bậc thấp như implementation của view.

Tại sao lại cần interface để cải thiện unit test? Bởi vì để có thể viết được unit test, tất cả code cần tập trung vào phạm vi ứng dụng của bạn thay vì quan tâm tới những hệ thống ngoài như SDK hay framework.

Hãy cùng xem đoạn ví dụ minh họa sau với trường hợp implement 1 màn hình login trong Android:

/**
* Login use case.Cho một email và password , thực hiện quá trình login.
*/
public class Login {

  private LoginService loginService;

  public Login(LoginService loginService) {
    this.loginService = loginService;
  }

  public void performLogin(String email, String password, LoginCallback callback) {
      boolean loginSuccess = loginService.performLogin(email, password);
      if (loginSuccess) {
        callback.onLoginSuccess();
      } else {
        callback.onLoginError();
      }
  }
}

/**
* LoginPresenter, nơi chúng ta implement những logic liên quan đến việc triển khai update UI.
*/
public class LoginPresenter {

    private LoginView view;
    private Login login;

    public LoginPresenter(LoginView view, Login login) {
        this.view = view;
        this.login = login;
    }

    public void onLoginButtonPressed(String email, String password) {
        if (!areUserCredentialsValid(email, password)) {
            view.showInvalidCredentialsMessage();
            return;
        }

        login.performLogin(email, password, new LoginCallback {
            void onLoginSuccess() {
                view.showLoginSuccessMessage();
            }

            void onLoginError() {
                view.showNetworkErrorMessage();
            }
        });
    }

}

/**
* Định nghĩa những gì presenter có thể làm với view mà không cần thiết phải phụ thuộc vào chi tiết implementation.
*/
public interface LoginView {

  void showLoginSuccessMessage()
  void showInvalidCredentialsMessage()
  void showNetworkErrorMessage()

}

public class LoginActivity extends Activity implements LoginView {

  .........

}

Làm ơn đừng chú ý đến syntax của đoạn code trên. Hầu như nó là pseudocode.

Tại sao chúng ta lại cần 1 interface cho view ở đây? Bởi vì như thế chúng ta có thể viết unit test thay thế cho implementation của view bằng 1 test double (thuật ngữ chung chỉ việc mock). Tại sao nó lại cần thiết trong bối cảnh của unit test? Bởi vì bạn không muốn phải mock cả Android SDK và sử dụng LoginActivity trong unit test. Hãy nhớ rằng bất cứ test nào mà Android SDK là 1 phần của hệ thống đang được test (SUT) thì không được gọi là unit test.

Một số lập trình viên đã quyết định là thêm cả 1 interface cho presenter. Nếu theo ví dụ ở trên thì implementation sẽ trông như thế này:

public interface LoginPresenter {

  void onLoginButtonPressed(String email, String password);
}

public class LoginPresenterImpl implements LoginPresenter {  
  ....
}

hoặc

public interface ILoginPresenter {

  void onLoginButtonPressed(String email, String password);
}

public class LoginPresenter implements ILoginPresenter {  
  ....
}

Có vấn đề gì với interface này? Theo quan điểm của tôi, interface này hoàn toàn không cần thiết và nó chỉ thêm vào độ phức tạp cho việc phát triển. Tại sao lại như vậy?

  • Hãy nhìn vào tên class. Khi interface này không cần thiết, tên của nó sẽ trở nên kì cục và không thêm được chút ngữ nghĩa nào cho code.
  • Interface đó là class mà chúng ta sẽ phải sửa để thêm vào những hàm mới khi logic của presentation có sự thay đổi. Một khi xong thì chúng ta cũng phải update cả implementation nữa. Kể cả khi chúng ta có sử dụng IDE tiên tiến nhất đi chăng nữa thì việc này cũng chỉ tổ tốn thời gian.
  • Việc theo dõi luồng code trở nên khó khăn hơn. Đó là bởi vì mỗi khi bạn đang ở trong 1 Activity (implementation của view) và muốn chuyển tới presenter, cái file mà bạn được chuyển đến sẽ là file interface nhưng hầu hết trong mọi trường hợp thì bạn lại muốn đi tới implementation.
  • Interface này không đóng góp cho việc cải thiện unit test. Các class presenter có thể được thay thế dễ dàng bởi 1 test double sử dụng bất cứ 1 thư viện mock nào hoặc thậm chí có thể làm bằng tay. Chúng ta không muốn phải viết test sử dụng activity như 1 SUT và thay thế presenter bằng 1 test double.

Vậy thì... thêm interface cho LoginPresenter chúng ta được gì? Chỉ thêm nhiễu 😃

Nhưng... Khi nào thì chúng ta nên dùng interface? Interface nên được dùng bất cứ khi nào chúng ta có nhiều hơn 1 implementation (trong trường hợp này, implementation cho presenter chỉ có 1). Và cả khi chúng ta cần phải tạo một giới hạn phạm vi giữa code của mình và framework hay SDK của bên thứ ba. Kể cả khi không dùng interface, chúng ta cũng có thể dùng kỹ thuật composition để tạo ra tính trừu tượng. Tuy vậy, sử dụng interface trong Java đơn giản hơn rất nhiều. Chúng tôi khuyến khích bạn nên thêm 1 interface nếu bạn gặp 1 trong 2 trường hợp trên. Ngoài ra thì đừng thêm code. Càng ít code thì maintain càng dễ. Nhớ rằng sử dụng interface không phải là cách duy nhất để tách rời code cho việc trừu tượng hóa.

Nếu tôi muốn tách rời phần implementation của view khỏi implementation của presenter thì sao? Đơn giản là bạn không cần phải làm vậy. Implementation của view là 1 chi tiết bậc thấp trong khi implementation của presenter là khái niệm trừu tượng bậc cao. Chi tiết implementation có thể được kết hợp với tính trừu tượng bậc cao. Bạn cần phải trừu tượng hóa mô hình miền ra khỏi framework mà trên đó nó được thực thi, nhưng bạn sẽ không muốn phải làm điều ngược lại. Cố gắng để giảm sự kết nối giữa implementation của view và presenter là việc lãng phí thời gian.

Bài viết được dịch ra từ Interfaces for presenters in MVP are a waste of time! của tác giả Pedro Vicente Gómez Sánchez.