Bài này được mình dịch từ đây

Bài dịch phần 2

Bạn có cảm thấy lạ khi thực hiện MVC trong iOS? Bạn có những nghi ngờ về việc chuyển sang MVVM? Bạn có nghe nói về VIPER, nhưng không chắc rằng nó có xứng đáng không?

Tiếp tục đọc, và bạn sẽ tìm thấy câu trả lời cho những câu hỏi trên, nếu bạn không tìm thấy hãy comment bên dưới

Bạn muốn ôn lại kiến thức của mình về các mô hình kiến trúc (architectural patterns) trong môi trường iOS. Chúng ta sẽ đánh giá ngắn gọn một số loại phổ biến và so sánh chúng trong lý thuyết và thực hành thông qua một vài ví dụ nhỏ. Hãy nhấn vào liên kết nếu bạn cần biết thêm thông tin chi tiết về bất kỳ loại nào.

Nắm vững nhiều mẫu thiết kế có thể gây nghiện, vì vậy hãy cẩn thận: bạn có thể sẽ tự hỏi nhiều câu hỏi hơn so với trước khi đọc bài viết này, kiểu như:

Bộ phận nào phải gửi networking request: Model hay Controller? Làm cách nào để truyền Model đến ViewModel của View mới? Bộ phận nào tạo ra một mô-đun VIPER mới: Router hay Presenter?

Tại sao phải quan tâm đến việc lựa chọn kiến trúc?

Bởi vì nếu bạn không quan tâm, vào một ngày đẹp trời, việc debug một class rất lớn với hàng tá thứ khác nhau, bạn sẽ thấy mình không có khả năng tìm và sửa chữa bất kỳ bug nào trong class của bạn. Tất nhiên, rất khó để nhớ hết class này trong đầu như toàn bộ entity, do đó, bạn sẽ luôn thiếu một vài chi tiết quan trọng. Nếu bạn đã gặp tình huống này với ứng dụng của bạn, nó rất có khả năng:

  • Class này là class con của UIViewController.
  • Dữ liệu của bạn được lưu trữ trực tiếp trong UIViewController.
  • UIView của bạn không có gì cả.
  • Model là một cấu-trúc dữ-liệu (data structure) ngớ ngẩn.
  • Unit Test của bạn cũng không có gì.

Và điều này có thể xảy ra, bất chấp thực tế là bạn đang làm theo hướng dẫn của Apple và thực hiện mô hình MVC của Apple, đừng lo lắng. Có một cái gì đó sai với mô hình MVC của Apple, nhưng chúng ta sẽ quay lại sau.

Hãy xác định những tính năng của một kiến trúc tốt:

  1. Sự phân chia (distribution) cân đối trách nhiệm giữa các entity với vai trò chặt chẽ.
  2. Khả năng kiểm thử (Testability) thường là tính năng đầu tiên cần tính đến (và đừng lo lắng: nó rất dễ dàng với kiến trúc phù hợp).
  3. Dễ sử dụng (ease of use) và chi phí bảo trì thấp.

Tại sao là sự phân chia?

Sự phân chia (distribution) giữ áp lực chia đều cho bộ não trong khi chúng ta cố gắng tìm cách cho mọi thứ hoạt động. Nếu bạn nghĩ rằng việc bạn phát triển sẽ tốt hơn việc bộ não sẽ thích ứng với sự hiểu biết phức tạp, bạn đúng. Nhưng có khả năng không thể mở rộng quy mô tuyến tính và đạt tới mức trần nhanh chóng. Vì vậy, cách dễ nhất để loại bỏ sự phức tạp là phân chia trách nhiệm giữa nhiều entity theo nguyên tắc trách nhiệm duy nhất (single responsibility principle).

Tại sao là khả năng kiểm thử?

Câu hỏi này không phải là câu hỏi của những người hay sử dụng Unit test, với việc thất bại sau khi thêm một vài tính năng mới hoặc là tái cấu trúc sự lộn xộn của class. Điều này có nghĩa là các developer lưu các bài test từ việc tìm kiếm vấn đề (issue) trong quá trình chạy (runtime), mà có thể xảy ra khi một ứng dụng trên thiết bị của người dùng và việc sửa chữa phải mất một thời gian mới đưa đến người dùng.

Tại sao là dễ sử dụng?

Câu hỏi này không đòi hỏi một câu trả lời nhưng code tốt nhất là code không bao giờ được viết. Do đó, càng ít code càng ít lỗi. Điều này có nghĩa là mong muốn viết code ít hơn không nên là sự giải thích cho sự lười biếng của developer, và bạn không nên ủng hộ một giải pháp là code ít mà chi phí bảo trì lớn.

MV(X) cơ bản

Hiện nay chúng ta có nhiều lựa chọn khi nói đến các mô hình thiết kế kiến trúc (architecture design patterns):

Ba mô hình đầu tiên cho rằng việc đưa các entity của ứng dụng vào một trong 3 loại:

  • Models — chịu trách nhiệm về dữ liệu hay một layer mà chịu trách nhiệm thao tác với các dữ liệu, ví dụ class ‘Person’ hoặc ‘PersonDataProvider’.
  • Views — chịu trách nhiệm cho các layer hiển thị (GUI), ở môi trường iOS, tất cả mọi thứ ở layer này bắt đầu với tiền tố “UI”.
  • Controller/Presenter/ViewModel — là chất keo kết dính hay là trung gian giữa ModelView, nói chung là chịu trách nhiệm về thay đổi Model bằng cách đáp ứng lại thao tác của người dùng trên View và cập nhật View với những thay đổi từ Model.

Việc chia thành 3 entity cho phép chúng ta:

  • hiểu về chúng tốt hơn
  • tái sử dụng lại chúng (chủ yếu áp dụng cho ViewModel)
  • kiểm thử chúng một cách độc lập

Hãy bắt đầu với mô hình MV(X) trước và quay lại mô hình VIPER sau.

MVC

Làm thế nào để sử dụng nó?

Trước khi thảo luận về MVC của Apple, nói một chút về MVC truyền thống

![MVC]()
MVC truyền thống MVC truyền thống

Trong trường hợp này, View có ít trạng thái hơn. Nó được render bởi Controller khi Model thay đổi. Hãy nghĩ đến trang web được tải lại (reloaded) hoàn toàn khi bạn nhấn vào link ở nơi nào đó. Mặc dù có thể thực hiện MVC truyền thống trong ứng dụng iOS, nó không có ý nghĩa gì nhiều do vấn đề kiến trúc - cả 3 entity được liên kết chặt chẽ, mỗi entity đều biết về 2 cái còn lại. Điều này làm giảm đáng kể khả năng tái sử dụng của mỗi entity - đó không phải là những gì bạn muốn có trong ứng dụng của mình. Vì lý do này, chúng ta bỏ qua phần viết ví dụ về nó.

MVC truyền thống có vẻ không được áp dụng trong quá trình phát triển ứng dụng iOS hiện tại.

MVC của Apple

Mong đợi

Cocoa MVC Cocoa MVC

Controller là trung gian giữa ViewModel, do đó chúng không biết gì về nhau nữa. Phần được tái sử dụng ít nhất là Controller và điều này thường là tốt, vì chúng ta phải có một nơi cho tất cả business logic mà không phù hợp với các Model.

Về lý thuyết, có vẻ rất đơn giản, nhưng bạn cảm thấy có cái gì sai sai, đúng không? Bạn nghe mọi người nói MVC như là Massive View Controller. Hơn nữa, việc giảm tải cho View Controller đã trở thành một chủ đề quan trọng đối với iOS developer. Tại sao điều này xảy ra nếu Apple lấy MVC truyền thống và cải thiện nó một chút?

MVC của Apple

Thực tế

Realistic Cocoa MVC Realistic Cocoa MVC

Cocoa MVC khuyến khích bạn viết Massive View Controllers, bởi vì họ cho là liên quan đến vòng đời của View (View’s life cycle) thật khó mà tách riêng được. Mặc dù bạn vẫn có khả năng giảm tải một số business logic và chuyển dữ liệu đến Model, bạn không có nhiều sự lựa chọn khi nói đến giảm tải cho View, tại hầu hết thời gian trách nhiệm của View là gửi thao tác của người dùng đến Controller. ViewController sẽ xử lý thông qua delegate và datasource và nó thường chịu trách nhiệm cho việc gửi đi (dispatching) và hủy bỏ (cancelling) các network request.

Bao nhiêu lần bạn đã thấy code như này:

swift
1
2
var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell
userCell.configureWithUser(user)

Cell được View cấu hình trực tiếp trong Model, vì vậy MVC guidelines bị vi phạm, nhưng điều này xảy ra mọi lúc, và thường thì mọi người không cảm thấy đó là sai. Nếu bạn nghiêm túc thực hiện các MVC, thì bạn phải cấu hình các cell từ Controller, và không vượt qua Model vào View, và điều này sẽ làm tăng kích thước của Controller nhiều hơn.

Cocoa MVC không phải viết tắt của Massive View Controller.

Vấn đề có thể không rõ ràng cho đến khi nói đến Unit Testing (hy vọng, nó có trong dự án của bạn). Kể từ khi ViewController được liên kết chặt chẽ với View, nó trở nên rất khó để kiểm thử bởi vì bạn phải rất sáng tạo trong View và vòng đời của nó, trong khi cách viết code của ViewController như vậy, làm cho business logic được tách ra càng nhiều càng tốt so với View.

Nhìn 1 ví dụ trong file playground:

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import UIKit

struct Person { // Model
    let firstName: String
    let lastName: String
}

class GreetingViewController : UIViewController { // View + Controller
    var person: Person!
    let showGreetingButton = UIButton()
    let greetingLabel = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)
    }

    func didTapButton(button: UIButton) {
        let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
        self.greetingLabel.text = greeting

    }
    // layout code goes here
}
// Assembling of MVC
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
view.person = model;

MVC assembling có thể được thực hiện trong view controller

Việc này làm khó khả năng kiểm thử đúng không? Chúng ta có thể di chuyển dữ liệu sang class GreetingModel và kiểm thử nó riêng lẻ, nhưng chúng ta không thể kiểm thử bất kỳ presentation logic nào bên trong GreetingViewController mà không gọi method liên quan trực tiếp đến UIView (viewDidLoad, didTapButton), mà có thể cần load tất cả View, điều này không tốt cho Unit Testing.

Thực tế, loading và testing UIView trên một simulator (như: iPhone 4S) không đảm bảo nó có thể hoạt động tốt trên các thiết bị khác (như: iPad). Do đó, tôi khuyến khích bạn nên loại bỏ “Host Application” ở phần cấu hình Unit Test và kiểm thử mà không cần chạy ứng dụng trên simulator.

Sự tương tác giữa ViewController làm cho khả năng kiểm thử với Unit Test trở nên khó khăn.

Với những gì đã nói ở trên, Cocoa MVC có vẻ là một bad pattern để được chọn. Nhưng chúng ta hãy đánh giá về những tính năng của nó được xác định trong phần đầu của bài viết:

  • Sự phân chia - ViewModel đã được tách riêng, nhưng ViewController lại được gắn chặt vào nhau.
  • Khả năng kiểm thử — do sự phân chia không được tốt nên bạn chỉ kiểm thử được phần Model.
  • Dễ sử dụng - với ít code hơn so với những mô hình khác. Ngoài ra, mọi người đã quen thuộc với nó, do đó, các developer ít kinh nghiệm cũng có thể dễ dàng bảo trì.

Cocoa MVC là pattern được chọn khi bạn không sẵn sàng đầu tư nhiều thời gian vào kiến trúc, và chi phí bảo trì là quá cao cho một project nhỏ.

Cocoa MVC là mô hình kiến trúc (architectural patterns) tốt nhất về khả năng cho ra sản phẩm nhanh nhất.

MVP

Passive View variant of MVP Passive View variant of MVP

Có phải nó giống hệt như MVC của Apple không ? Đúng là giống, nó có tên là MVP (Passview View). Đợi đã, thế có nghĩa là MVC của Apple trong thực tế là một MVP hả? Không phải, vì ở MVC của Apple thì View gắn chặt với Controller, trong khi phần trung gian của MVPPresenter, mà phần này thì không đụng chạm gì đến vòng đời của ViewController, và View có thể tách riêng dễ dàng. Presenter không liên quan đến phần layout nữa, nhưng nhiệm vụ của nó là cập nhật data và state cho View.

Nếu tôi nói với bạn, UIViewController chính là View.

Phần khái niệm của MVP, UIViewController là class con của View và nó không phải là Presenter. Sự phân chia này làm cho khả năng kiểm thử tốt hơn, và tốc độ phát triển sản phẩm cũng nhanh hơn, vì bạn phải tạo dữ liệu thủ công và ràng buộc với sự kiện, bạn có thể thấy ở ví dụ dưới đây:

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import UIKit

struct Person { // Model
    let firstName: String
    let lastName: String
}

protocol GreetingView: class {
    func setGreeting(greeting: String)
}

protocol GreetingViewPresenter {
    init(view: GreetingView, person: Person)
    func showGreeting()
}

class GreetingPresenter : GreetingViewPresenter {
    unowned let view: GreetingView
    let person: Person
    required init(view: GreetingView, person: Person) {
        self.view = view
        self.person = person
    }
    func showGreeting() {
        let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
        self.view.setGreeting(greeting)
    }
}

class GreetingViewController : UIViewController, GreetingView {
    var presenter: GreetingViewPresenter!
    let showGreetingButton = UIButton()
    let greetingLabel = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)
    }

    func didTapButton(button: UIButton) {
        self.presenter.showGreeting()
    }

    func setGreeting(greeting: String) {
        self.greetingLabel.text = greeting
    }

    // layout code goes here
}
// Assembling of MVP
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
let presenter = GreetingPresenter(view: view, person: model)
view.presenter = presenter

Lưu ý quan trọng liên quan đến assembly

MVP là mô hình đầu tiên cho thấy vấn đề lắp ráp mà xảy ra ở 3 layer thực sự riêng biệt. Chúng ta không muốn View biết về Model, điều này không đúng để thực hiện assembly bên trong ViewController (đây là View), do đó chúng ta phải thực hiện việc này ở chỗ khác. Ví dụ, chúng ta có thể tạo ra Router service để chịu trách nhiệm cho việc thực hiện assembly và trình bày View-to-View. Vấn đề phát sinh này đã được giải quyết không chỉ ở MVP mà còn ở các mô hình sau.

Cùng nhìn lại các tính năng của MVP:

  • Sự phân chia - hầu hết trách nhiệm được phân chia giữa PresenterModel, với phần View khá ngớ ngẩn (trong ví dụ trên phần Model cũng khá ngớ ngẩn)
  • Khả năng kiểm thử - là tuyệt vời, chúng ta có thể kiểm thử hầu hết các business logic.
  • Dễ sử dụng - trong ví dụ đơn giản có phần không thực tế ở trên, số lượng dòng code nhiều hơn gấp đôi so với MVC, nhưng phần ý tưởng của MVP rất rõ ràng.

MVP trong iOS có khả năng kiểm thử tuyệt vời và nhiều dòng code hơn.

MVP

Với Bindings và Hooters

Ở một kiểu khác của MVP - Supervising Controller MVP. Phiên bản này bao gồm binding trực tiếp giữa ViewModel trong khi Presenter (Supervising Controller) vẫn xử lý các thao tác từ View và có khả năng thay đổi các View.

Supervising Presenter variant of the MVP Supervising Presenter variant of the MVP

Nhưng như chúng ta đã biết, phân chia trách nhiệm không rõ ràng, cũng như gắn chặt ViewModel là không tốt. Điều đó cũng tương tự như cách phát triển phần mềm trên desktop của Cocoa.

Tương tự như MVC truyền thống, tôi sẽ không đưa ra ví dụ cho kiến trúc chưa hoàn thiện. (còn nữa)