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

Khi bạn không biết đến kiến trúc, một ngày nọ, gỡ lỗi một class lớn với hàng tá thứ khác nhau, bạn sẽ thấy mình không thể tìm và sửa bất kỳ lỗi nào trong các class. Đương nhiên, thật khó để giữ cho class này hoạt động ổn định trong toàn bộ thực thể, do đó, bạn sẽ luôn thiếu một số chi tiết quan trọng. Nếu bạn đã ở trong tình huống này với ứng dụng của mình, rất có khả năng:

  • Class bị rối của bạn lại là một lớp con của class UIViewController
  • Dữ liệu của bạn được lưu trực tiếp trong UIViewController nên khó chỉnh sửa
  • UIView của bạn được tạo ra không phục vụ mục đích nào 
  • Cấu trúc dữ liệu model rối rắm
  • Mặc dù Unit Tests của bạn không gặp vấn đề gì

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

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

  • Phân phối cân bằng trách nhiệm giữa các thực thể với vai trò nghiêm ngặt.
  • Khả năng kiểm tra thường đến từ tính năng đầu tiên (và đừng lo lắng: thật dễ dàng với kiến trúc phù hợp).
  • Dễ sử dụng và chi phí bảo trì thấp.

Tại sao phải phân phối?

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

Tại sao phải kiểm tra?

Đây thường không phải là một câu hỏi cho những người đã cảm thấy biết ơn đối với các bài kiểm tra trên UnitTest, đã thất bại sau khi thêm các tính năng mới hoặc do tái cấu trúc một số điều phức tạp của lớp. Điều này có nghĩa là các thử nghiệm đã lưu các nhà phát triển đó khỏi việc tìm kiếm các sự cố trong thời gian chạy, tuy nhiên sự cố có thể xảy ra với ứng dụng trên thiết bị của người dùng và việc khắc phục phải mất một tuần để tiếp cận người dùng.

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

Điều này không yêu cầu câu trả lời nhưng điều đáng nói là mã tốt nhất là mã chưa bao giờ được viết. Do đó, khi càng có ít mã, thì càng có ít lỗi. Điều này có nghĩa là mong muốn viết ít mã hơn không bao giờ được giải thích chỉ bởi sự lười biếng của lập trình viên và nên ủng hộ một giải pháp thông minh hơn nhắm mắt vào chi phí bảo trì của nó.

Yếu tố cần thiết cho MV (X)

Ngày nay chúng ta có nhiều lựa chọn khi nói đến các mẫu thiết kế kiến ​​trúc, một vài kiến trúc được nêu trong bài viết này:

Ba kiến trúc đầu tiên giả định đưa các thực thể của ứng dụng vào một trong 3 loại:

Model - chịu trách nhiệm về dữ liệu miền hoặc lớp truy cập thao túng dữ liệu

View - chịu trách nhiệm về lớp trình bày (GUI), đối với môi trường iOS là những đối tượng bắt đầu bằng tiền tố UI.

Controller / Presenter / ViewModel - sự kết hợp hoặc bộ trung gian giữa Model và View, nói chung chịu trách nhiệm thay đổi Model bằng cách phản ứng với các hành động của người dùng được thực hiện trên View và cập nhật View với các thay đổi từ Model.

Có các thực thể được chia cho phép chúng ta:

  • hiểu họ hơn (như chúng ta đã biết)
  • sử dụng lại chúng (chủ yếu áp dụng cho View và Model)
  • kiểm tra chúng một cách độc lập

Hãy bắt đầu với các mẫu MV (X) và quay lại VIPER sau.

MVC

Sử dụng nó như thế nào?

Trước khi thảo luận về tầm nhìn của Apple, hãy xem MVC theo một cách truyền thống.

MVC truyền thống

Trong trường hợp này, View không có trạng thái. Nó chỉ được trình điều khiển hiển thị một khi Model được thay đổi. Hãy nghĩ về trang web được tải lại hoàn toàn sau khi bạn nhấn vào liên kết để điều hướng đến một nơi khác. Mặc dù có thể triển khai MVC truyền thống trong ứng dụng iOS, nhưng điều này không có ý nghĩa gì do vấn đề kiến trúc - cả ba thực thể được liên kết chặt chẽ, mỗi thực thể đều biết về hai thực thể kia. Điều này làm giảm đáng kể khả năng sử dụng lại của mỗi phần trong số chúng - đó không phải là thứ chúng ta muốn có trong ứng dụng của mình. 

Apple’s MVC

Sự mong đợi

Cocoa MVC

ViewController là một trung gian giữa View và Model để chúng không biết về nhau. Ít sử dụng nhất là ViewController và điều này thường tốt cho chúng tôi, vì chúng tôi phải có một vị trí cho tất cả logic phức tạp không phù hợp với Model.
Về lý thuyết, nó có vẻ rất đơn giản, nhưng bạn cảm thấy có gì đó không ổn, phải không? Bạn thậm chí đã nghe thấy mọi người không viết tắt MVC là Massive View Controller. Hơn nữa, giảm tải ViewController đã trở thành một chủ đề quan trọng đối với các nhà phát triển iOS. Tại sao điều này xảy ra nếu Apple chỉ lấy MVC truyền thống và cải thiện nó một chút?

Cocoa MVC khuyến khích bạn viết Massive View Controller, bởi vì chúng liên quan đến vòng đời của View, đến nỗi khó có thể nói chúng tách rời nhau. Mặc dù bạn vẫn có khả năng giảm tải một số logic và chuyển đổi dữ liệu cho Model, nhưng bạn không có nhiều sự lựa chọn khi thực hiện giảm tải công việc cho View, hầu hết mọi lúc, trách nhiệm của View là gửi hành động đến ViewController. Khi ViewController kết thúc là một đại biểu và nguồn dữ liệu của mọi thứ cũng kết thúc, và nó thường chịu trách nhiệm gửi và hủy các yêu cầu mạng.
Đã bao nhiêu lần bạn thấy mã như thế này:

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

Cell, là khung nhìn được cấu hình trực tiếp với Model, do đó, các nguyên tắc MVC bị vi phạm, nhưng điều này xảy ra mọi lúc và mọi người thường không cảm thấy điều đó là sai. Nếu bạn tuân thủ nghiêm ngặt MVC, thì bạn phải cấu hình cell từ bộ điều khiển và lồng chuyển Model vào View và điều này sẽ tăng kích thước của ViewController của bạn hơn nữa.

Cocoa MVC is reasonably unabbreviated as the Massive View Controller.

Vấn đề có thể không rõ ràng cho đến khi nói đến Thử nghiệm đơn vị (hy vọng, nó có trong dự án của bạn). Do bộ điều khiển chế độ xem của bạn được kết hợp chặt chẽ với chế độ xem, nên việc kiểm tra trở nên khó khăn vì bạn phải rất sáng tạo trong chế độ xem chế độ xem và vòng đời của chúng, trong khi viết mã của trình điều khiển chế độ xem theo cách đó, logic kinh doanh của bạn bị tách biệt nhiều càng tốt từ mã bố trí xem.
Hãy để một cái nhìn về ví dụ sân chơi đơn giản:

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 can be performed in the presenting view controller

Điều này có vẻ rất khó kiểm tra, phải không? Chúng ta có thể chuyển thế hệ lời chào vào lớp Thiệp mới và kiểm tra riêng, nhưng chúng ta không thể kiểm tra bất kỳ logic trình bày nào (mặc dù trong ví dụ trên không có nhiều logic như vậy) mà không gọi trực tiếp các phương thức liên quan đến UIView ( viewDidLoad, didTapButton) có thể gây tải tất cả các chế độ xem và điều này không tốt cho thử nghiệm đơn vị.
Trên thực tế, việc tải và kiểm tra UIView trên một trình giả lập (ví dụ iPhone 4S) không đảm bảo rằng nó sẽ hoạt động tốt trên các thiết bị khác (ví dụ: iPad), vì vậy tôi khuyên bạn nên xóa Ứng dụng máy chủ lưu trữ trên máy tính khỏi cấu hình mục tiêu Kiểm tra đơn vị của bạn và chạy thử nghiệm của bạn mà không có ứng dụng của bạn chạy trên trình giả lập.

Với tất cả những gì đã nói, có vẻ như Cốc Cốc MVC là một mẫu khá tệ để lựa chọn. Nhưng hãy để bên lề đánh giá nó về các tính năng được xác định ở đầu bài viết:

Phân phối - Chế độ xem và Mô hình trên thực tế tách biệt, nhưng Chế độ xem và Bộ điều khiển được kết hợp chặt chẽ.
Khả năng kiểm tra - do phân phối xấu mà bạn có thể chỉ kiểm tra Mô hình của bạn.
Dễ sử dụng - số lượng mã ít nhất trong số các mẫu khác. Ngoài ra, mọi người đều quen thuộc với nó, do đó, nó dễ dàng duy trì ngay cả bởi các nhà phát triển chưa có kinh nghiệm.

Ca cao MVC là mô hình lựa chọn của bạn nếu bạn chưa sẵn sàng đầu tư nhiều thời gian hơn vào kiến ​​trúc của mình và bạn cảm thấy rằng một cái gì đó với chi phí bảo trì cao hơn là quá mức cho dự án thú cưng nhỏ bé của bạn.

Cocoa MVC is the best architectural pattern in terms of the speed of the development.

MVVM

Mới nhất và lớn nhất của loại MV (X)

MVVM là loại MV (X) mới nhất, do đó, hãy hy vọng nó xuất hiện có tính đến các vấn đề mà MV (X) đang gặp phải trước đây.
Về lý thuyết, Model-View-ViewModel trông rất tốt. Chế độ xem và Mô hình đã quen thuộc với chúng tôi, nhưng cũng là Người hòa giải, được biểu diễn dưới dạng Mô hình xem.

MVVM

MVVM coi bộ điều khiển xem là View

Không có khớp nối chặt chẽ giữa Chế độ xem và Mô hình

Ngoài ra, nó không ràng buộc như phiên bản Giám sát của MVP; tuy nhiên, lần này không phải giữa Chế độ xem và Mô hình mà là giữa Chế độ xem và Mô hình Chế độ xem.
Vậy View Model trong thực tế iOS là gì? Về cơ bản, nó là đại diện độc lập của UIKit cho Chế độ xem và trạng thái của bạn. Mô hình Chế độ xem gọi các thay đổi trong Mô hình và tự cập nhật với Mô hình được cập nhật và vì chúng ta có một ràng buộc giữa Chế độ xem và Mô hình xem, lần đầu tiên được cập nhật tương ứng.

Ràng buộc:

Tôi đã đề cập ngắn gọn về chúng trong phần MVP, nhưng hãy để thảo luận về chúng một chút ở đây. Các ràng buộc ra khỏi hộp để phát triển OS X, nhưng chúng tôi không có chúng trong hộp công cụ iOS. Tất nhiên chúng tôi có KVO và thông báo, nhưng chúng không thuận tiện như ràng buộc.
Vì vậy, với điều kiện chúng tôi không muốn tự viết chúng, chúng tôi có hai lựa chọn:

Một trong những thư viện ràng buộc dựa trên KVO như RZDataBinding hoặc SwiftBond

Các con thú lập trình phản ứng chức năng quy mô đầy đủ như ReactiveCocoa, RxSwift hoặc PromiseKit.

Trên thực tế, hiện nay, nếu bạn nghe thấy MVVM - bạn nghĩ ReactiveCocoa và ngược lại. Mặc dù có thể xây dựng MVVM với các ràng buộc đơn giản, ReactiveCocoa (hoặc anh chị em) sẽ cho phép bạn nhận được hầu hết MVVM.
Có một sự thật cay đắng về khuôn khổ phản ứng: sức mạnh to lớn đi kèm với trách nhiệm cao cả. Nó rất dễ làm hỏng mọi thứ khi bạn phản ứng. Nói cách khác, nếu bạn làm điều gì đó sai, bạn có thể mất nhiều thời gian để gỡ lỗi ứng dụng.

Trong ví dụ đơn giản của chúng tôi, khung FRF hoặc thậm chí KVO là quá mức cần thiết, thay vào đó chúng tôi sẽ yêu cầu Mô hình xem cập nhật bằng cách sử dụng phương thức showGreet và sử dụng thuộc tính đơn giản cho chức năng gọi lại helloDidChange.

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

Trở lại với đánh giá trước đó:

Phân phối - không rõ ràng trong ví dụ nhỏ bé của chúng tôi, nhưng trên thực tế, Chế độ xem MVVM có nhiều trách nhiệm hơn Chế độ xem MVP. Bởi vì cái đầu tiên cập nhật trạng thái của nó từ Mô hình Chế độ xem bằng cách thiết lập các ràng buộc, khi cái thứ hai chỉ chuyển tiếp tất cả các sự kiện đến Người trình bày và không cập nhật chính nó.
Khả năng kiểm tra - Mô hình Chế độ xem không biết gì về Chế độ xem, điều này cho phép chúng tôi kiểm tra dễ dàng. Chế độ xem cũng có thể được kiểm tra, nhưng vì nó phụ thuộc vào UIKit nên bạn có thể muốn bỏ qua nó.
Dễ sử dụng - nó có cùng số lượng mã như MVP trong ví dụ của chúng tôi, nhưng trong ứng dụng thực tế nơi bạn phải chuyển tiếp tất cả các sự kiện từ Chế độ xem sang Người thuyết trình và để cập nhật Chế độ xem theo cách thủ công, MVVM sẽ dễ dàng hơn nhiều nếu bạn sử dụng các ràng buộc.

The MVVM is very attractive, since it combines benefits of the aforementioned approaches, and, in addition, it doesn’t require extra code for the View updates due to the bindings on the View side. Nevertheless, testability is still on a good level.

Tham khảo tại medium