Viper和MVVM架构比较:如何同时应用



当前,VIPER和MVVM是用于大型应用程序开发的最受欢迎的体系结构解决方案,需要参与测试,长期支持和不断发展的大型团队的开发。 在本文中,我们将尝试将它们应用于一个小型测试项目,该项目是具有添加新联系人功能的用户联系人列表。 本文比分析具有更多的实践性,并且主要针对理论上已经熟悉这些体系结构并且现在希望了解特定示例如何工作的人员。 但是,也提供了体系结构及其比较的基本描述。


本文是Rafael Sacchi 文章“比较MVVM和Viper架构:何时使用一种或另一种”的译文。 不幸的是,在本文创建的某个时候,设置了“出版物”而不是“翻译”,因此您必须在此处编写。

精心设计的架构对于确保对项目的持续支持至关重要。 在本文中,我们将探讨MVVM和VIPER体系结构作为传统MVC的替代方案。

对于所有从事软件开发相当一段时间的人来说,MVC是一个众所周知的概念。 这种模式将项目分为三个部分:代表实体的模型; 视图,它是用户交互的界面; 和控制器,负责确保视图和模型之间的交互。 这是Apple为我们提供的可在应用程序中使用的体系结构。

但是,您可能知道项目具有相当大而复杂的功能:支持网络请求,解析,访问数据模型,将数据转换为输出,对接口事件做出反应等。 结果,您将获得可以解决上述任务的大型控制器以及一堆无法重用的代码。 换句话说,对于拥有长期项目支持的开发人员来说,MVC可能是一场噩梦。 但是如何确保iOS项目的高度模块化和可重用性呢?

我们将研究MVC架构的两个非常著名的替代方案:MVVM和VIPER。 两者在iOS社区中都很有名,并且证明它们可以替代MVC。 我们将讨论它们的结构,编写一个示例应用程序,并考虑哪种情况下最好使用一种或另一种体系结构。

例子

我们将编写一个带有用户联系表的应用程序。 您可以使用此存储库中的代码。 在Starter文件夹中,包含项目的基本框架,在Final文件夹中,包含完整的应用程序。

该应用程序将有两个屏幕:在第一个屏幕上,将在表中显示联系人列表,在单元格中,将显示联系人的名字和姓氏,以及基本图片而不是用户的图片。



第二个屏幕是用于添加新联系人的屏幕,带有名字和姓氏输入字段以及“完成”和“取消”按钮。



MVVM

运作方式:

MVVM代表Model-View-ViewModel 。 这种方法与MVC的区别在于模块之间的责任分配逻辑。

  • 型号 :此模块与MVC中的模块相同。 他负责创建数据模型,并且可能包含业务逻辑。 您还可以创建帮助程序类,例如,用于在Model中管理对象的管理器类和用于处理网络请求和解析的网络管理器。
  • 视图 :此处一切开始改变。 MVVM中的View模块涵盖了界面(UIView,.xib和.storyboard文件的子类),显示逻辑(动画,渲染)和用户事件的处理(按钮单击等)。在MVC中,View和Controller负责。 这意味着您拥有的视图将保持不变,而ViewController将只包含MVC中视图的一小部分,因此将大大减少。
  • ViewModel :现在这里是您以前在ViewController中拥有的大多数代码的位置。 ViewModel层从模型请求数据(可以是对本地数据库的请求,也可以是网络请求),然后将其传输回View,该格式已经是它们将在其中使用和显示的格式。 但这是双向机制,用户输入的动作或数据通过ViewModel传递并更新Model。 由于ViewModel跟踪显示的所有内容,因此在两层之间使用链接机制非常有用。


与MVC相比,您正在从看起来像这样的体系结构迁移:



下一个架构变化:



其中,UIView和UIViewController的类和子类用于实现View。

好吧,到现在为止。 让我们使用MVVM体系结构编写一个应用程序示例。

MVVM联系人应用程序

型号

下列类是Contact联系人模型:

import CoreData open class Contact: NSManagedObject { @NSManaged var firstName: String? @NSManaged var lastName: String? var fullName: String { get { var name = "" if let firstName = firstName { name += firstName } if let lastName = lastName { name += " \(lastName)" } return name } } } 


联系人类具有字段firstNamelastName以及计算的fullName属性。

查看

VIEW包括:主情节提要板,已经放置了视图; ContactsViewController,它在表中显示联系人列表; 以及带有一对标签和输入字段的AddContactViewController,以添加新联系人的姓名和姓氏。 让我们从ContactsViewController开始。 其代码如下所示:

 import UIKit class ContactsViewController: UIViewController { @IBOutlet var tableView: UITableView! let contactViewModelController = ContactViewModelController() override func viewDidLoad() { super.viewDidLoad() tableView.tableFooterView = UIView() contactViewModelController.retrieveContacts({ [unowned self] in self.tableView.reloadData() }, failure: nil) } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { let addContactNavigationController = segue.destination as? UINavigationController let addContactVC = addContactNavigationController?.viewControllers[0] as? AddContactViewController addContactVC?.contactsViewModelController = contactViewModelController addContactVC?.didAddContact = { [unowned self] (contactViewModel, index) in let indexPath = IndexPath(row: index, section: 0) self.tableView.beginUpdates() self.tableView.insertRows(at: [indexPath], with: .left) self.tableView.endUpdates() } } } extension ContactsViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell") as? ContactsTableViewCell guard let contactsCell = cell else { return UITableViewCell() } contactsCell.cellModel = contactViewModelController.viewModel(at: (indexPath as NSIndexPath).row) return contactsCell } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return contactViewModelController.contactsCount } } 


即使只是粗略地看一眼,很明显此类也实现了大多数接口任务。 它还在prepareForSegue(::)方法中具有导航功能-这正是添加路由器层时VIPER发生变化的时刻。

让我们仔细看一下实现UITableViewDataSource协议的类扩展。 函数不能直接与Model层中的Contact用户的联系人模型一起使用-而是以已经显示的形式接收数据(由ContactViewModel结构表示),该数据已经使用ViewModelController进行了格式化。

同样的事情发生在电路中,该电路在创建接触后立即开始。 他唯一的任务是在表中添加一行并更新接口。

现在,您需要在UITableViewCell的子类和ViewModel之间建立关系。 这看起来像ContactsTableViewCell表的单元格类:

 import UIKit class ContactsTableViewCell: UITableViewCell { var cellModel: ContactViewModel? { didSet { bindViewModel() } } func bindViewModel() { textLabel?.text = cellModel?.fullName } } 


AddContactViewController类也是如此:

 import UIKit class AddContactViewController: UIViewController { @IBOutlet var firstNameTextField: UITextField! @IBOutlet var lastNameTextField: UITextField! var contactsViewModelController: ContactViewModelController? var didAddContact: ((ContactViewModel, Int) -> Void)? override func viewDidLoad() { super.viewDidLoad() firstNameTextField.becomeFirstResponder() } @IBAction func didClickOnDoneButton(_ sender: UIBarButtonItem) { guard let firstName = firstNameTextField.text, let lastName = lastNameTextField.text else { return } if firstName.isEmpty || lastName.isEmpty { showEmptyNameAlert() return } dismiss(animated: true) { [unowned self] in self.contactsViewModelController?.createContact(firstName: firstName, lastName: lastName, success: self.didAddContact, failure: nil) } } @IBAction func didClickOnCancelButton(_ sender: UIBarButtonItem) { dismiss(animated: true, completion: nil) } fileprivate func showEmptyNameAlert() { showMessage(title: "Error", message: "A contact must have first and last names") } fileprivate func showMessage(title: String, message: String) { let alertView = UIAlertController(title: title, message: message, preferredStyle: .alert) alertView.addAction(UIAlertAction(title: "Ok", style: .destructive, handler: nil)) present(alertView, animated: true, completion: nil) } } 


同样,这里主要进行与UI的合作。 请注意,AddContactViewController在didClickOnDoneButton(:)函数中将联系人创建功能委托给ViewModelController。

查看模型

现在该为我们讨论全新的ViewModel层。 首先,创建一个ContactViewModel联系人类,该类将提供我们需要显示的视图,并且将定义带有参数的<and>函数以对联系人进行排序:

 public struct ContactViewModel { var fullName: String } public func <(lhs: ContactViewModel, rhs: ContactViewModel) -> Bool { return lhs.fullName.lowercased() < rhs.fullName.lowercased() } public func >(lhs: ContactViewModel, rhs: ContactViewModel) -> Bool { return lhs.fullName.lowercased() > rhs.fullName.lowercased() } 


ContactViewModelController代码将如下所示:

 class ContactViewModelController { fileprivate var contactViewModelList: [ContactViewModel] = [] fileprivate var dataManager = ContactLocalDataManager() var contactsCount: Int { return contactViewModelList.count } func retrieveContacts(_ success: (() -> Void)?, failure: (() -> Void)?) { do { let contacts = try dataManager.retrieveContactList() contactViewModelList = contacts.map() { ContactViewModel(fullName: $0.fullName) } success?() } catch { failure?() } } func viewModel(at index: Int) -> ContactViewModel { return contactViewModelList[index] } func createContact(firstName: String, lastName: String, success: ((ContactViewModel, Int) -> Void)?, failure: (() -> Void)?) { do { let contact = try dataManager.createContact(firstName: firstName, lastName: lastName) let contactViewModel = ContactViewModel(fullName: contact.fullName) let insertionIndex = contactViewModelList.insertionIndex(of: contactViewModel) { $0 < $1 } contactViewModelList.insert(contactViewModel, at: insertionIndex) success?(contactViewModel, insertionIndex) } catch { failure?() } } } 


注意: MVVM没有提供有关如何创建ViewModel的确切定义。 当我想创建更分层的体系结构时,我更喜欢创建一个ViewModelController,它将与Model层交互并负责创建ViewModel对象。

最容易记住的主要事情是:ViewModel层不应参与用户界面的使用。 为了避免这种情况,最好不要使用ViewModel将UIKit导入到文件中。

ContactViewModelController类从本地存储请求联系人,并尝试不影响Model层。 它以视图需要显示的格式返回数据,并在添加新联系人并且数据更改时通知视图。

在现实生活中,这将是一个网络请求,而不是对本地数据库的请求,但在任何情况下都不应该将其作为ViewModel的一部分-网络工作和与本地数据库一起使用均应使用其自己的管理器来提供(经理)。

MVVM就是这样。 在您看来,这种方法可能比MVC更可测试,支持和分发。 现在,让我们谈谈VIPER,看看它与MVVM有何不同。

VIPER

运作方式:

VIPER是适用于iOS项目的Clean Architecture实现。 它的结构包括:视图,交互器,演示者,实体和路由器。 这实际上是一个非常分布式的模块化体系结构,允许您分担责任,单元测试很好地覆盖了其中,并使您的代码可重用。

  • View :通常包含UIKit文件(包括UIViewController)的接口层。 可以理解,在更多分布式系统中,UIViewController的子类应与View相关。 在VIPER中,事情几乎与MVVM中的事情相同:View负责显示Presenter提供的内容,并将用户输入的信息或操作传输到Presenter。
  • Interactor :包含应用程序正常工作所需的业务逻辑。 Interactor负责从模型(网络或本地请求)中检索数据,并且其实现与用户界面无关。 重要的是要记住,网络和本地管理者不是VIPER的一部分,而是被视为独立的依赖项。
  • 演示者 :负责格式化数据以显示在视图中。 在我们的示例中的MVVM中,ViewModelController对此负责。 Presenter从Interactor接收数据,创建ViewModel实例(用于正确显示的格式化类),并将其传递给View。 他还响应用户输入的数据,从数据库请求其他数据,反之亦然,将其传递给她。
  • 实体 :负责模型层的职责,该层在其他体系结构中使用。 实体是一个简单的数据对象,没有业务逻辑,由在线拖拉机和各种数据管理器管理。
  • 路由器 :所有应用程序导航逻辑。 似乎这不是最重要的层,但是例如,如果您需要在iPhone和iPad应用程序上重用同一视图,则唯一可以更改的是视图在屏幕上的显示方式。 这样一来,您无需触摸路由器以外的其他任何层,而在每种情况下,路由器都会对此负责。


与MVVM相比,VIPER在职责分配上有几个主要差异:

-他有一个路由器,一个单独的层,负责导航

-实体是简单的数据对象,将访问数据的责任从模型重新分配给交互器

-ViewModelController职责在Interactor和Presenter之间共享

现在,让我们重复相同的应用程序,但已经在VIPER上了。 但是为了便于理解,我们将仅使控制器带有接触。 您可以使用链接( 此存储库中的 VIPER Contacts Starter文件夹)找到用于在项目中添加新联系人的控制器的代码。

注意 :如果决定在VIPER上创建项目,则不应尝试手动创建所有文件-可以使用其中一种代码生成器,例如VIPER GenGeneramba(Rambler项目)

VIPER联系人应用程序

查看

VIEW由Main.storyboard和ContactListView类的元素表示。 VIEW是非常被动的; 他的唯一任务是在收到Presenter的通知后将接口事件转移到Presenter并更新其状态。 这是ContactListView代码的样子:

 import UIKit class ContactListView: UIViewController { @IBOutlet var tableView: UITableView! var presenter: ContactListPresenterProtocol? var contactList: [ContactViewModel] = [] override func viewDidLoad() { super.viewDidLoad() presenter?.viewDidLoad() tableView.tableFooterView = UIView() } @IBAction func didClickOnAddButton(_ sender: UIBarButtonItem) { presenter?.addNewContact(from: self) } } extension ContactListView: ContactListViewProtocol { func reloadInterface(with contacts: [ContactViewModel]) { contactList = contacts tableView.reloadData() } func didInsertContact(_ contact: ContactViewModel) { let insertionIndex = contactList.insertionIndex(of: contact) { $0 < $1 } contactList.insert(contact, at: insertionIndex) let indexPath = IndexPath(row: insertionIndex, section: 0) tableView.beginUpdates() tableView.insertRows(at: [indexPath], with: .right) tableView.endUpdates() } } extension ContactListView: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell") else { return UITableViewCell() } cell.textLabel?.text = contactList[(indexPath as NSIndexPath).row].fullName return cell } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return contactList.count } } 


视图将viewDidLoaddidClickOnAddButton事件发送到Presenter。 在第一个事件中,Presenter将向Interactor请求数据,在第二个事件中,Presenter将要求路由器切换到控制器以添加新联系人。

当请求联系人列表或添加新联系人时,从Presenter中调用ContactListViewProtocol协议方法。 无论哪种情况,“视图”中的数据仅包含显示所需的信息。

视图中还包含实现UITableViewDataSource协议的方法,该协议使用接收到的数据填充表。

交互者

在我们的示例中,Interactor非常简单。 他所做的只是通过本地数据库管理器请求数据,对于他来说,此管理器,CoreData,Realm或任何其他解决方案使用什么都无关紧要。 ContactListInteractor中的代码如下:

 class ContactListInteractor: ContactListInteractorInputProtocol { weak var presenter: ContactListInteractorOutputProtocol? var localDatamanager: ContactListLocalDataManagerInputProtocol? func retrieveContacts() { do { if let contactList = try localDatamanager?.retrieveContactList() { presenter?.didRetrieveContacts(contactList) } else { presenter?.didRetrieveContacts([]) } } catch { presenter?.didRetrieveContacts([]) } } } 


Interactor收到请求的数据后,将通知Presenter。 另外,作为一种选择,Interactor可以将错误传输给Presenter,Presenter然后必须将错误格式化为适合在View中显示的视图。

注意 :您可能已经注意到,VIPER中的每个层都实现了一个协议。 结果,类依赖于抽象,而不依赖于特定的实现,因此符合依赖关系反转的原理(SOLID的原理之一)。

演讲者

建筑最重要的元素。 视图与其余各层(交互器和路由器)之间的所有通信均通过Presenter。 ContactListPresenter代码:

 class ContactListPresenter: ContactListPresenterProtocol { weak var view: ContactListViewProtocol? var interactor: ContactListInteractorInputProtocol? var wireFrame: ContactListWireFrameProtocol? func viewDidLoad() { interactor?.retrieveContacts() } func addNewContact(from view: ContactListViewProtocol) { wireFrame?.presentAddContactScreen(from: view) } } extension ContactListPresenter: ContactListInteractorOutputProtocol { func didRetrieveContacts(_ contacts: [Contact]) { view?.reloadInterface(with: contacts.map() { return ContactViewModel(fullName: $0.fullName) }) } } extension ContactListPresenter: AddModuleDelegate { func didAddContact(_ contact: Contact) { let contactViewModel = ContactViewModel(fullName: contact.fullName) view?.didInsertContact(contactViewModel) } func didCancelAddContact() {} } 


加载View后,它会通知Presenter,Presenter进而通过Interactor请求数据。 当用户单击“添加新联系人”按钮时,View会通知演示者,该演示者发送一个请求以在路由器中打开“添加新联系人”屏幕。

Presenter还会格式化数据,并在查询联系人列表后将其返回到“视图”。 他还负责实现AddModuleDelegate协议。 这意味着在添加新联系人时,演示者将收到通知,准备要显示的联系人数据并传输到View。

您可能已经注意到,Presenter很有可能变得非常麻烦。 如果有这种可能性,那么Presenter可以分为两个部分:Presenter,它仅接收数据,格式化数据以进行显示并将其传递给View; 还有一个事件处理程序,它将响应用户的操作。

实体

该层类似于MVVM中的模型层。 在我们的应用程序中,它由Contact类和操作员定义函数<and>表示。 联系人内容将如下所示:

 import CoreData open class Contact: NSManagedObject { var fullName: String { get { var name = "" if let firstName = firstName { name += firstName } if let lastName = lastName { name += " " + lastName } return name } } } public struct ContactViewModel { var fullName = "" } public func <(lhs: ContactViewModel, rhs: ContactViewModel) -> Bool { return lhs.fullName.lowercased() < rhs.fullName.lowercased() } public func >(lhs: ContactViewModel, rhs: ContactViewModel) -> Bool { return lhs.fullName.lowercased() > rhs.fullName.lowercased() } 


ContactViewModel包含Presenter填充(视图显示)的字段(格式)。 Contact类是NSManagedObject的子类,包含与CoreData模型中相同的字段。

路由器

最后,最后一层,但绝对不重要。 导航的全部责任在于Presenter和WireFrame。 演示者从用户那里收到一个事件,并且知道何时进行过渡,而WireFrame知道如何以及在何处进行过渡。 为避免混淆,在此示例中,路由器层由ContactListWireFrame类表示,在本文中称为WireFrame。 ContactListWireFrame代码:

 import UIKit class ContactListWireFrame: ContactListWireFrameProtocol { class func createContactListModule() -> UIViewController { let navController = mainStoryboard.instantiateViewController(withIdentifier: "ContactsNavigationController") if let view = navController.childViewControllers.first as? ContactListView { let presenter: ContactListPresenterProtocol & ContactListInteractorOutputProtocol = ContactListPresenter() let interactor: ContactListInteractorInputProtocol = ContactListInteractor() let localDataManager: ContactListLocalDataManagerInputProtocol = ContactListLocalDataManager() let wireFrame: ContactListWireFrameProtocol = ContactListWireFrame() view.presenter = presenter presenter.view = view presenter.wireFrame = wireFrame presenter.interactor = interactor interactor.presenter = presenter interactor.localDatamanager = localDataManager return navController } return UIViewController() } static var mainStoryboard: UIStoryboard { return UIStoryboard(name: "Main", bundle: Bundle.main) } func presentAddContactScreen(from view: ContactListViewProtocol) { guard let delegate = view.presenter as? AddModuleDelegate else { return } let addContactsView = AddContactWireFrame.createAddContactModule(with: delegate) if let sourceView = view as? UIViewController { sourceView.present(addContactsView, animated: true, completion: nil) } } } 


由于WireFrame负责创建模块,因此在此处配置所有依赖项将很方便。 当您要打开另一个控制器时,打开新控制器的函数会将要打开它的对象作为参数接收,并使用其WireFrame创建一个新控制器。 同样,在创建新控制器时,将必要的数据传输到该控制器,在这种情况下,只有代表(具有联系人的控制器的演示者)才能接收创建的联系人。

路由器层提供了一个很好的机会,可以避免在情节提要中使用segue(过渡)并组织所有代码导航。 由于情节提要板没有提供用于在控制器之间传输数据的紧凑解决方案,因此我们的导航实施不会添加额外的代码。 我们获得的只是最佳的可重用性。


总结

您可以在此存储库中找到两个项目。

如您所见,MVVM和VIPER尽管有所不同,但并不是唯一的。 MVVM告诉我们,除了View和Model外,还应该有一个ViewModel层。 但是,对于应如何创建此层,以及如何请求数据,都没有任何说明-没有明确定义该层的责任。 有很多方法可以实现它,您可以使用其中任何一种。

另一方面,VIPER是一个相当独特的体系结构。 它由许多层组成,每个层都有一个明确定义的职责范围,并且少于MVVM受开发人员影响的范围。

在选择架构时,通常不是唯一正确的解决方案,但我仍然会尝试提供一些技巧。 如果您有一个庞大而漫长的项目,并有明确的要求,并且希望有足够的机会重用组件,那么VIPER将是最佳的解决方案。 更加清晰的职责划分可以更好地组织测试并提高重用性。

Source: https://habr.com/ru/post/zh-CN440904/


All Articles