Android Architecture Patterns (phần 1): Model-View-Controller

Cách đây 1 năm, phần lớn team Android hiện tại của tôi đã bắt tay làm ứng dụng upday – 1 ứng dụng không mạnh mẽ, cũng không ổn định như mong đợi. Chúng tôi đã cố gắng tìm hiểu lý do tại sao kết quả các dòng code của mình lại ra như thế và đã tìm ra được 2 nguyên nhân chính: do UI thay đổi liên tục và thiếu 1 architecture hỗ trợ linh hoạt. Ứng dụng hiện đã được redesign lần thứ 4 trong 6 tháng. Design pattern được chọn có vẻ như là Model-View-Controller nhưng lại xảy ra “đột biến” khiến mọi thứ không như kì vọng.

Bài viết dưới đây sẽ giải thích rõ Model-View-Controller pattern là gì; cách chúng được áp dụng vào Android trong những năm qua; làm thế nào để tối ưu khả năng test và 1 số ưu, nhược điểm của MVC.

Pattern Model-View-Controller

Trong 1 thế giới mà logic giao diện người dùng có xu hướng thay đổi nhiều hơn business logic thì các lập trình viên desktop, web cần phải tìm cách tách biệt functionality trong giao diện người dùng. Pattern MVC chính là giải pháp đó.

  • Model  – layer dữ liệu, chịu trách nhiệm quản lý business logic và xử lý network hoặc database API
  • View  – UI Layer, visualisation của dữ liệu từ Model
  • Controller  –  logic layer nhận thông tin từ hành vi của người dùng và cập nhật Model khi cần thiết

Model-View-Controller class structure

Như vậy, cả Controller và View đều phụ thuộc vào Model: Controller để cập nhật dữ liệu, View để cập nhật dữ liệu. Nhưng, quan trọng nhất với các dev desktop và web vào thời điểm trước đây chính là: Model được tách biệt và có thể được test độc lập với UI. Rất nhiều variants của MVC đã xuất hiện. Những variants tốt nhất đều liên quan tới liệu Model chủ động hay bị động thông báo nó đã thay đổi. Chi tiết như sau:

Passive Model

Trong phiên bản Passive Model, Controller là class duy nhất vận dụng Model. Dựa trên actions của user, Controller phải điều chỉnh Model. Sau khi Model được update, Controller sẽ thông báo View cần phải update. Lúc này, View sẽ request data từ Model.

Active Model

Trong trường hợp Controller không phải là class duy nhất modify Model, Model cần phải có 1 cách để thông báo đến View và các classes khác về các updates. Pattern Observer sẽ hỗ trợ điều này. Model gồm tập hợp các observers quan tâm đến các updates. View implement giao diện observer và đăng kí như 1 observer của Model.

Model-View-Controller — active Model — class structure

Mỗi khi Model cập nhật, Model sẽ iterate qua bộ các observers và gọi method update. Quá trình implement của method này trong View sau đó sẽ kích hoạt request dữ liệu mới nhất từ Model.

Model-View-Controller — active Model — behavior

Model-View-Controller trong Android

Năm 2011, khi Android bắt đầu trở nên nổi tiếng hơn, các câu hỏi architecture cũng tự nhiên xuất hiện. Vì MVC là 1 trong những patterns UI nổi tiếng nhất thời gian đó nên các lập trình viên cũng đã cố gắng áp dụng vào Android.

Nếu bạn tìm trên StackOverflow những câu hỏi như: “Làm cách nào để áp dụng MVC vào Android”, 1 trong những câu trả lời phổ biến nhất là trong Android, 1 Activity gồm cả View và Controller. Câu trả lời nghe có vẻ điên rồ nhưng thời điểm đó, trọng tâm trính là tạo Model tes được và lựa chọn implementation với View và Controller lại phụ thuộc vào platform.

MVC được áp dụng như thế nào trong Android?

Ngày nay, câu hỏi làm thế nào để áp dụng pattern MVC đã có 1 câu trả lời dễ dàng hơn nhiều. Các Activities, Fragments và Views nên là Views trong thế giới MVC. Controller nên là các classes riêng biết không extend hoặc sử dụng bất kì class Android nào, tương tự với Models.

Một vấn đề nổi lên khi kết nối Controller với View, vì Controller cần phải thông báo View để update. Trong architecture Model MVC passive, Controllers cần phải giữ 1 tham chiếu đến View. Tuy tập trung vào testing, cách dễ nhất để thực hiện được điều này là phải có giao diện BaseView mà Activity/Fragment/View sẽ extend. Vì vậy, Controller sẽ có 1 tham chiếu đến BaseView.

Ưu điểm

Pattern Model-View-Controller sẽ phân tán bớt các vấn đề. Ưu điểm này không chỉ tăng khả năng test của code mà còn dễ extend hơn, cho phép implementation các tính năng mới khá dễ dàng.

Các classes Model không có bất kì tham chiếu nào đến các classes Android, mà tiến thẳng đến unit test. Controller không extend hay implement bất kì Android classes vào và nên có 1 tham chiếu đến interface class của View. Bằng cách này, unit testing của Controller là hoàn toàn có thể.

Nếu Views tuân theo nguyên tắc trách nhiệm duy nhất (single responsibility principle), sau đó vai trò của chúng chỉ để update Controller cho mỗi user event và chỉ hiển thị dữ liệu từ Model mà không implement bất kì business logic nào. Trong trường hợp này, nên có đủ UI tests để đáp ứng được hết các functionalities của View.

Khuyết điểm

View phụ thuộc vào Controller và Model

Việc phụ thuộc của View vào Model ban đầu là 1 mặt trái trong Views phức tạp. Để tối thiểu logic trong View, Model nên cung cấp các methods test được cho mỗi yếu tố cần phải hiển thị. Trong 1 implementation Model active, việc này sẽ tăng số lượng classes và methods theo cấp số nhân cho dù các Observers cho mỗi loại dữ liệu là bắt buộc.

Dù View phụ thuộc vào cả Controller và Model, những thay đổi trong UI logic đòi hỏi những cập nhật trong nhiều classes, làm giảm tính linh hoạt của pattern.

Ai sẽ xử lý UI logic?

Theo pattern MVC, Controller cập nhật Model và View lấy dữ liệu để được hiển thị từ Model. Nhưng ai sẽ là người quyết định cách hiển thị dữ liệu? Là Model hay View? Hãy cân nhắc ví dụ sau: chúng ta có 1 User, với tên và họ. Trong View, chúng ta cần hiển thị user name như “Lastname, Firstname” (Ví dụ: “Doe, John”).

Nếu vai trò của Model chỉ là cung cấp dữ liệu “thô”, đồng nghĩa là code trong View sẽ như thế này:

String firstName = userModel.getFirstName(); 
String lastName = userModel.getLastName(); 
nameTextView.setText(lastName + ", " + firstName)

Vậy View sẽ chịu trách nhiệm xử lý UI logic nhưng lúc này UI logic sẽ không thể unit test.

Cách tiếp cận khác là để Model chỉ được tiếp cận với dữ liệu cần phải hiển thị, giấu business logic bất kì khỏi View. Nhưng sau đó, hệ quả là Models sẽ xử lý cả business logic và UI logic. Nó sẽ unit test được nhưng sau khi Model xong sẽ ngầm hiểu là phụ thuộc vào View.

String name = userModel.getDisplayName(); 
nameTextView.setText(name);

Kết luận

Trong thời gian đầu của Andorid, pattern Model-View-Controller có vẻ khá rối rắm với nhiều lập trình viên, dẫn đến tình trạng code khó khăn hoặc không thể thực hiện unit test.

Sự phụ thuộc của View với Model và logic trong View dẫn code-base đến 1 trạng thái không thể phục hồi mà không refactor toàn bộ ứng dụng. Đâu là cách tiếp cận mới trong architecture? Đọc thêm blog  này.

Bài viết được dịch bởi tech talk via medium

Nguyễn Linh

Chia sẻ để cùng tiến bộ...