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

Bài dịch phần 1.

MVVM

Mới nhất và tuyệt vời nhất trong mô hình MV(X)

MVVM là mới nhất trong kiểu MV(X), hãy hy vọng nó sẽ giải quyết được các vấn đề mà MV(X) đối mặt trước đây.

Về lý thuyết Model-View-ViewModel rất tốt. ViewModel đã quen thuộc với chúng ta, nhưng còn phần trung gian, là ViewModel.

MVVM MVVM

Nó khá giống với MVP:

  • MVVM xử lý ViewController như là View.
  • Không có liên kết chặt chẽ giữa ViewModel.

Ngoài ra, nó binding giống như phiên bản Supervising của MVP. Tuy nhiên, không phải giữa ViewModel mà là giữa ViewViewModel.

Các ViewModel trong iOS thực tế là gì? Đó là UIKit đại diện cho View và các state của nó. ViewModel sẽ gọi ra những thay đổi trong Model và cập nhật chính nó với bản Model mới cập nhật và chúng ta có 1 binding giữa ViewViewModel.

Bindings

Tôi đã giới thiệu vắn tắt trong phần MVP, nhưng chúng ta hãy thảo luận một chút ở đây. Bindings được lấy ra từ quá trình phát triển phần mềm của OSX, nhưng chúng ta không có nó trong công cụ của iOS. Tất nhiên là chúng ta có KVOnotification, nhưng nó không thuận tiện như binding.

Vì vậy, tôi không muốn nói về chúng nữa, chúng ta có 2 lựa chọn:

Trong thực tế, khi bạn nghe thấy “MVVM” - bạn nghĩ ngay đến ReactiveCocoa và ngược lại. Mặc dù nó có thể được xây dựng dựa trên MVVM với các binding đơn giản, ReactiveCocoa (hoặc anh chị em của nó) sẽ cho phép bạn sử dụng hầu hết các phần của MVVM.

Có một sự thật cay đắng về reactive framework là khả năng lớn đi kèm với trách nhiệm lớn. Nó thực sự dễ lộn xộn khi bạn reactive. Nói cách khác, khi bạn làm điều gì đó sai, bạn có thể phải dành nhiều thời gian cho việc gỡ lỗi cho ứng dụng, vì vậy cần có 1 cái nhìn tổng quan về call stack.

Reactive Debugging Reactive Debugging

Trong ví dụ dưới, FRF framework hoặc KVO là quá mức cần thiết, thay vì chúng ta yêu cầu rõ ràng View Model cập nhật sử dụng method showGreeting và sử dụng property cho callback của greetingDidChange.

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
import UIKit

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

protocol GreetingViewModelProtocol: class {
    var greeting: String? { get }
    var greetingDidChange: ((GreetingViewModelProtocol) -> ())? { get set } // function to call when greeting did change
    init(person: Person)
    func showGreeting()
}

class GreetingViewModel : GreetingViewModelProtocol {
    let person: Person
    var greeting: String? {
        didSet {
            self.greetingDidChange?(self)
        }
    }
    var greetingDidChange: ((GreetingViewModelProtocol) -> ())?
    required init(person: Person) {
        self.person = person
    }
    func showGreeting() {
        self.greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
    }
}

class GreetingViewController : UIViewController {
    var viewModel: GreetingViewModelProtocol! {
        didSet {
            self.viewModel.greetingDidChange = { [unowned self] viewModel in
                self.greetingLabel.text = viewModel.greeting
            }
        }
    }
    let showGreetingButton = UIButton()
    let greetingLabel = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.showGreetingButton.addTarget(self.viewModel, action: "showGreeting", forControlEvents: .TouchUpInside)
    }
    // layout code goes here
}
// Assembling of MVVM
let model = Person(firstName: "David", lastName: "Blaine")
let viewModel = GreetingViewModel(person: model)
let view = GreetingViewController()
view.viewModel = viewModel

Và cùng đánh giá các tính năng của MVVM:

  • Sự phân chia - nó không rõ ràng trong ví dụ ở trên, nhưng thực tế View của MVVM có nhiều trách nhiệm hơn View của MVP. Bởi vì View của MVVM cập nhật từ ViewModel bằng cách cài đặt binding, View của MVP chỉ chuyển tiếp các sự kiện đến Presenter và không cập nhật chính nó.
  • Khả năng kiểm thử - ViewModel không biết gì về View, điều này cho phép chúng ta dễ dàng kiểm thử. View cũng có thể được kiểm thử, nhưng nó phụ thuộc vào UIKit làm bạn có thể muốn bỏ qua nó.
  • Dễ sử dụng - nó có số lượng dòng code tương tự như MVP trong ví dụ trên, nhưng trong thực tế, bạn muốn chuyển tiếp tất cả các sự kiện từ View đến Presenter và cập nhật View thủ công, MVVM có thể ít code hơn nếu bạn sử dụng binding.

MVVM rất hấp dẫn, vì nó kết hợp điểm mạnh của các phương pháp nói trên và nó không đòi hỏi code thêm cho cập nhật View do các binding ở phần View. Tuy nhiên, khả năng kiểm thử vẫn còn tốt.

VIPER

Áp dụng kinh nghiệm chơi LEGO vào thiết kế iOS app

VIPER là ứng viên cuối của chúng ta, nó đặc biệt thú vị vì nó không thuộc loại MV(X).

Từ bây giờ bạn phải đồng ý rằng mức độ chi tiết trong trách nhiệm là rất tốt. VIPER lặp lại ý tưởng phân chia trách nhiệm, và lần này chúng ta có 5 layer.

VIPER VIPER
  • Interactor - chứa các business logic liên quan đến data (Entities) hoặc networking, giống như tạo các instance mới của entity hoặc fetching chúng từ server. Để áp dụng bạn sẽ sử dụng một vài Services và Managers mà không được xem như là phần của mô-đun VIPER mà chỉ là 1 phần mở rộng bên ngoài.
  • Presenter - có chứa các business logic có liên quan đến UI(nhưng độc lập với UIKit), gọi các method trong Interactor.
  • Entities - là những đối tượng dữ liệu (data objects) của bạn, không phải là lớp truy cập dữ liệu (data access layer) bởi vì đó là trách nhiệm của Interactor.
  • Router - chịu trách nhiệm cho việc định tuyến (segues) giữa các mô-đun VIPER.

Về cơ bản, module VIPER có thể là một màn hình hoặc toàn bộ các user story ứng dụng của bạn - nghĩ về xác thực, đó có thể là một màn hình hoặc một vài màn hình liên quan.

Nếu chúng ta so sánh nó với kiểu MV(X), chúng ta sẽ thấy một vài điểm khác về sự phân chia trách nhiệm:

  • Model (tương tác dữ liệu) logic chuyển vào Interactor với các entities như những cấu-trúc dữ-liệu (data structure) ngớ ngẩn.
  • Nhiệm vụ biểu diễn UI của Controller/Presenter/ViewModel được di chuyển vào Presenter, nhưng không có khả năng thay đổi dữ liệu.
  • VIPER là mô hình đầu tiên giải quyết một cách rõ ràng trách nhiệm điều hướng, và được thực hiện bởi Router.

Việc định tuyến một cách đúng đắn là một thách thức đối với các ứng dụng iOS, mô hình MV(X) không giải quyết được vấn đề này.

Ví dụ dưới không bao gồm định tuyến (routing) hay tương tác giữa các module, giống như những ví dụ của các mô hình thuộc MV(X).

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import UIKit

struct Person { // Entity (usually more complex e.g. NSManagedObject)
    let firstName: String
    let lastName: String
}

struct GreetingData { // Transport data structure (not Entity)
    let greeting: String
    let subject: String
}

protocol GreetingProvider {
    func provideGreetingData()
}

protocol GreetingOutput: class {
    func receiveGreetingData(greetingData: GreetingData)
}

class GreetingInteractor : GreetingProvider {
    weak var output: GreetingOutput!
    
    func provideGreetingData() {
        let person = Person(firstName: "David", lastName: "Blaine") // usually comes from data access layer
        let subject = person.firstName + " " + person.lastName
        let greeting = GreetingData(greeting: "Hello", subject: subject)
        self.output.receiveGreetingData(greeting)
    }
}

protocol GreetingViewEventHandler {
    func didTapShowGreetingButton()
}

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

class GreetingPresenter : GreetingOutput, GreetingViewEventHandler {
    weak var view: GreetingView!
    var greetingProvider: GreetingProvider!
    
    func didTapShowGreetingButton() {
        self.greetingProvider.provideGreetingData()
    }
    
    func receiveGreetingData(greetingData: GreetingData) {
        let greeting = greetingData.greeting + " " + greetingData.subject
        self.view.setGreeting(greeting)
    }
}

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

    override func viewDidLoad() {
        super.viewDidLoad()
        self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)
    }
    
    func didTapButton(button: UIButton) {
        self.eventHandler.didTapShowGreetingButton()
    }
    
    func setGreeting(greeting: String) {
        self.greetingLabel.text = greeting
    }
    
    // layout code goes here
}
// Assembling of VIPER module, without Router
let view = GreetingViewController()
let presenter = GreetingPresenter()
let interactor = GreetingInteractor()
view.eventHandler = presenter
presenter.view = view
presenter.greetingProvider = interactor
interactor.output = presenter

Một lần nữa, cùng nhìn lại với các tính năng:

  • Sự phân chia - không nghi ngờ gì nữa, VIPER là nhà vô địch trong phân chia trách nhiệm.
  • Khả năng kiểm thử - không có gì ngạc nhiên, phân chia tốt hơn kéo theo khả năng kiểm thử tốt hơn.
  • Dễ sử dụng - Cuối cùng, hai điều ở trên kéo theo chi phí bảo trì như thế nào bạn cũng đoán được. Nhưng bạn phải viết số lượng rất lớn interface cho các class với những trách nhiệm rất nhỏ.

Những gì về LEGO?

Trong khi sử dụng VIPER, bạn có cảm giác giống như xây dựng toà nhà Empire State từ những khối trò chơi LEGO, và đó là dấu hiệu cho thấy bạn đang có vấn đề. Có lẽ, còn quá sớm để áp dụng VIPER cho ứng dụng của bạn và bạn nên cân nhắc cái gì đó đơn giản hơn. Một số người bỏ qua điều này và tiếp tục bắn pháo vào một con chim sẻ [0]. Tôi đoán họ tin rằng các ứng dụng của họ sẽ được hưởng lợi từ VIPER ít nhất là trong tương lai, ngay cả khi hiện nay chi phí bảo trì cao bất hợp lý. Nếu bạn cũng tin như vậy, tôi muốn khuyên bạn nên thử dùng Generamba - một công cụ tạo bộ khung cho VIPER. Đối với tôi nó giống như việc sử dụng hệ thống nhắm tự động cho khẩu pháo thay vì đơn giản là dùng ná cao su.

Cùng so sánh về mặt tính năng giữa các mô hình kiến trúc ta đã nói ở trên qua slide của tác giả

So sánh MVC MVP MVVM VIPER Redux-like So sánh MVC MVP MVVM VIPER Redux-like

Kết luận

Chúng ta đã tìm hiểu qua một số mô hình kiến trúc, và tôi hy vọng bạn đã tìm thấy câu trả lời cho những gì bạn cần, nhưng bạn cần biết rằng không có viên đạn bạc [1] cho việc lựa chọn mô hình kiến trúc, đây là vấn đề của sự cân bằng trong tình huống cụ thể của bạn.

Vì vậy, cũng là tự nhiên khi kết hợp nhiều mô hình kiến trúc trong cùng một ứng dụng. Ví dụ: bạn đã bắt đầu với MVC, sau đó bạn nhận ra rằng một màn hình cụ thể đã trở nên quá khó khăn để bảo trì hiệu quả với MVC và chuyển sang MVVM, nhưng chỉ áp dụng riêng màn hình đặc biệt này. Không cần tái cấu trúc lại các màn hình khác mà MVC thực sự không làm việc tốt, bởi vì cả hai kiến trúc có thể dễ dàng tương thích với nhau.


[0]. Chú thích của người dịch: tác giả ám chỉ đến việc sử dụng “dao mổ trâu để giết gà”

[1]. silver bullet, xuất phát từ những câu chuyện về người sói. Người sói được cho là dễ bị thương hoặc có thể chết vì viên đạn bạc.