新的iOS软件体系结构:4V引擎

原始文章:https://marcosantadev.com/new-ios-software-architecture-4v-engine/

您应该阅读这篇文章吗?

本文介绍的是一种新的软件体系结构,它具有比VIPER和MVVM-C更高的层次。 这意味着它可能比其他已知体系结构更复杂。

如果您想继续阅读本文,则必须接受我的观点,即如果我们想要一个干净且可测试的体系结构,则我们应该有几层责任。

我不想将其作为可以解决您所有问题的完美架构来出售。 它可能无法完全满足您的需求。 出于这个原因,我建议您通读这篇文章,并自己判断这种体系结构对您的项目是否有意义。

如果您想知道为什么MVC不足以进行iOS开发,建议您跳入为什么MVC不足。

介绍

我创建了这个博客,撰写了有关MVVM-C的文章。 然后,我写了一篇关于SOLID原理的文章。 在这一点上,您可能会认为我没有实践我的讲道。 如果我说的是“单一责任原则”,那么即使Coordinator层承担多个责任,为什么我仍要使用MVVM-C? 它创建堆栈( View ModelViewService ),在父UIKit组件中添加View并确定路由添加/删除子协调器。 MVVM-C必须进行一些重构,以免违反“单一职责原则”。

在本文中,我将解释主要的iOS软件体系结构的替代方案: 4V Engine

祝您阅读愉快!

为什么MVC不够

我在上一篇有关MVVM-C的文章中已经介绍了这一点,但是我想再次写出来,因为我认为它非常重要。

成为iOS开发人员的一种常见方法是查看文档,并按照Apple建议的模式编写一些简单项目。 这意味着我们大多数人已经开始使用MVC作为软件架构来创建我们的第一个应用程序。 逐步地,我们开始习惯于MVC。 在学习过程的这一点上,我们认为MVC是正确的方法。 它起作用了,为什么我们要迁移到另一个增加代码复杂性的体系结构中?

主要有两个原因:

  1. SOLID原则:视图控制器的职责过多。
  2. 可测试性:视图控制器与UIKit紧密结合,因此很难进行测试。

如果我们想解决以上问题,则应该采用另一种架构。 不幸的是,没有什么是免费的。 这种改变有一个代价:复杂性。 如果我们查看VIPER和MVVM-C,我们会注意到有多个层次可以管理并让它们一起通信。 如果我们拥有简单的应用程序,或者我们实际上并不关心SOLID和可测试性,则可能会适得其反。

我个人的观点是:

我喜欢编写遵循SOLID原则并经过适当测试以尽可能避免错误的编写良好的代码。 因此,我想花时间和精力来创建新的干净软件体系结构。

MVVM-C的问题

如前所述,MVVM-C还不够。 Coordinator层违反了“单一责任原则”,必须分为三个部分,承担以下职责:

  • 管理应用程序路由。
  • 创建堆栈( View ModelViewService )。
  • 在父UIKit组件中显示View

分解Coordinator想法使我转向4V Engine。

我要感谢我的朋友 Ennio Masi 指出了这个 Coordinator 问题,这促使我找到了解决方案。

VIPER的问题

另一个著名的iOS架构是VIPER。 它具有几层,与MVVM-C非常相似,只是命名不同。

如果分析VIPER,我们会发现MVVM-C的相同问题。 有罪的层称为Wireframe ,它是体系结构的路由器。

为了便于说明,我们使用从VIPER-SWIFT复制的以下代码:

 class AddWireframe: NSObject, UIViewControllerTransitioningDelegate { 
  var addPresenter : AddPresenter? 
var presentedViewController : UIViewController?

func presentAddInterfaceFromViewController(_ viewController: UIViewController) {
let newViewController = addViewController()
newViewController.eventHandler = addPresenter
newViewController.modalPresentationStyle = .custom
newViewController.transitioningDelegate = self

viewController.present(newViewController, animated: true, completion: nil)

presentedViewController = newViewController
}

//.....

我们可以注意到presentAddInterfaceFromViewController太多:

  1. 管理路由。
  2. 创建View及其属性。
  3. 在父UIViewController显示View

我们注意到,就这三点而言,我们遇到了与MVVM-C Coordinator相同的问题。

4V引擎

我们已经分析了常见的iOS软件架构,并且发现了一些问题。 如果我们愿意开发人员并且希望改进我们的代码,那么我们将需要一种重构先前代码的替代方法。 出于这个目标,我创建了这个新的软件架构:

不用担心,这可能会造成混淆,但是我们很快将对每个层进行解释。

如上图所示,该架构的核心由View PresenterView FactoryViewView Model ,因此该架构被称为4 V引擎。

入门

现在,是时候解释每个单独的层了。 由于我认为跳入代码是学习知识的最佳方法,因此我们将使用示例应用程序通过示例覆盖每一层。

您可以在这里找到Github仓库。

这是一个非常简单的应用程序,包含两个组件:

  • 用户列表:从远程API获取的用户列表。
  • 用户详细信息:具有在用户列表中选择的用户名的视图-我们可以在用户列表中点击UITableViewCell的信息按钮来选择用户。

层数

我认为解释这些层的最佳方法是从底部( Model )到顶部( Router )。 开始吧。

模型

该模型表示我们应用程序的数据。

在示例应用程序中,我们有一个模型User

 struct User { 
let name: String
}

代表从API响应中解析出的单个用户。

互动者

Interactor器与VIPER中使用的相同。

该层管理Model ,以为View Model准备数据。 View Model不应直接在模型上执行任何操作,但应委托Interactor进行任何数据操作。

在示例应用程序中,我们有一个交互器,该交互器HTTPClient服务HTTPClient从远程API获取用户,然后将在User数组中接收到的JSON数据转换为多亏了助手类UsersParser以便在View Model

 final class UsersListInteractor { 
private let httpClient: HTTPClientType

init(httpClient: HTTPClientType = HTTPClient()) {
self.httpClient = httpClient
}

func fetchUsers(completion: @escaping ([User]) -> Void) {

guard let url = URL(string: "https://jsonplaceholder.typicode.com/users") else {
completion([])
return
}

let httpCompletionHandler: (Data) -> Void = { data in
guard let jsonData = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [[String: Any]] else {
completion([])
return
}

let users = UsersParser.parse(jsonData)
completion(users)
}

httpClient.get(at: url, completionHandler: httpCompletionHandler)
}
}

查看模型

我们可以认为View Model是该体系结构中最重要的层。 它的职责是与UI交互,以决定在UI操作后显示什么以及如何表现。

该层不应包含任何UIKit引用。 如果我们想在ViewView Model之间进行通信,则应该使用UI绑定机制。 我已经在上一篇文章中介绍了主要机制。 对于此示例应用程序,我决定避免使用RxSwift进行绑定,因为它会增加示例的复杂性。 为了使所有内容尽可能简单,使用委托模式已实现了绑定。

我们可以将View Model及其Interactor一起使用,以将数据显示在UI中,如示例应用程序所示:

 // Delegate used to bind the UI and the View Model 
protocol UsersListViewModelDelegate: class {
func usersListUpdated()
}
 final class UsersListViewModel { 
  // Value used in View to know how many table rows to show 
var usersCount: Int {
return users.count
}
  private weak var delegate: UsersListViewModelDelegate? 
private weak var navigationDelegate: UsersListNavigationDelegate?
  private let usersListInteractor: UsersListInteractor 
private var users = [User]()
  init(usersListInteractor: UsersListInteractor, navigationDelegate: UsersListNavigationDelegate) { 
self.navigationDelegate = navigationDelegate
self.usersListInteractor = usersListInteractor
  loadUsers() 
}
  // Asks the interactor the list of users to show in the UI 
private func loadUsers() {
usersListInteractor.fetchUsers { [unowned self] users in
self.users = users
  DispatchQueue.main.async { 
// Method used to ask the View to update the table view with the new data
self.delegate?.usersListUpdated()
}
}
}
  private func user(at indexPath: IndexPath) -> User { 
return users[indexPath.row]
}
  // Sets the delegate to bind the UI 
func bind(_ delegate: UsersListViewModelDelegate) {
self.delegate = delegate
}
  // Method used in View to know which user name to show in the cell 
func userName(at indexPath: IndexPath) -> String {
let user = self.user(at: indexPath)
return user.name
}
  // Method called in View when the user taps a cell detail button 
func userDetailsSelected(at indexPath: IndexPath) {
let user = self.user(at: indexPath)
  // Method used to notify the router that a user has been selected 
navigationDelegate?.usersListSelected(for: user)
}
}

使用MVC,我们可以将业务逻辑保留在视图控制器中。 借助4V Engine,我们可以在View Model内部移动业务逻辑并轻松对其进行测试,因为我们与UIKit没有依赖关系。

注意:

  • navigationDelegate用于与Router通信。 我们将在路由器中看到它。
  • 方法bind用于ViewView Model之间的UI绑定。 我们将在“视图”中看到它。

视图

View层表示用于在设备屏幕上显示内容的任何UIKit组件。

在示例应用程序中, View是用于用户详细信息的UIViewController和用于用户列表的UITableViewController

好的架构的优点是我们可以轻松地测试我们的图层。 View通常最难测试,因为它与依赖项UIKit结合在一起。 因此,我们必须使该层尽可能平淡无奇,并在可测试的层中移动业务逻辑。 “可测试”层是View Model 。 正如我们在View Model中看到的那样,UI数据由View Model驱动。 这样,我们可以在View Model内部移动业务逻辑。 View变成一个愚蠢的图层,仅用于在设备屏幕上显示某些内容。

使用View理解的重要概念是UI绑定,它使我们能够设置View ModelView之间的通信。 如果您不知道什么是UI绑定,请看一下我以前的文章。

这是示例应用程序中的View的示例:

 class UsersListTableViewController: UITableViewController { 
  // The view model used for the binding 
private unowned let viewModel: UsersListViewModel
  init(viewModel: UsersListViewModel) { 
self.viewModel = viewModel
  super.init(nibName: nil, bundle: nil) 
}
  required init?(coder aDecoder: NSCoder) { 
fatalError("init(coder:) has not been implemented")
}
  override func viewDidLoad() { 
super.viewDidLoad()
  let nib = UINib.init(nibName: "UsersListTableViewCell", bundle: nil) 
tableView.register(nib, forCellReuseIdentifier: "Cell")
  // Binds View and View Model 
viewModel.bind(self)
}
  // MARK: - Table view data source 
  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 
// Asks the View Model how many users are available
return viewModel.usersCount
}
  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
  // If it's the custom cell, configure it 
if let usersListCell = cell as? UsersListTableViewCell {
// Asks the View Model the user name for a specific index path
let userName = viewModel.userName(at: indexPath)
// Sets the user name
usersListCell.configure(userName: userName)
}
  return cell 
}
  override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) { 
// Notifies the View Model that a detail button has been tapped
viewModel.userDetailsSelected(at: indexPath)
}
}

在本示例中可以看到,UI绑定通常是双向的,有时我们向View Model请求一些数据,有时又被View Model通知以更新UI,例如使用usersListUpdated

注意:

属性viewModel具有关键字viewModel 。 需要避免保留周期。 由于View Factory已经保留了View Model的强大参考(正如我们将在View Factory中看到的那样),因此View不需要为其View Model保持强大的参考。

查看工厂

到目前为止,这些层与VIPER和MVVM-C非常相似。 现在,是时候解释一下乍一看可能令人困惑的图层了。

View Factory的职责是创建架构的核心: ViewView ModelInteractor

单独使用View Factory可能没有多大意义,我们必须在正确的上下文中查看它。 我们将在View Presenter中了解其用法。

让我们从示例应用程序中查看一个示例:

 final class UsersListViewFactory { 
  let viewController: UsersListTableViewController 
private let viewModel: UsersListViewModel
  init(navigationDelegate: UsersListNavigationDelegate) { 
let interactor = UsersListInteractor()
viewModel = UsersListViewModel(usersListInteractor: interactor, navigationDelegate: navigationDelegate)
viewController = UsersListTableViewController(viewModel: viewModel)
}
}

注意:

  • 我们公开了要在View Presenter使用的viewController 。 我们也可能出于特定原因公开View Model 。 我认为大多数时候它可以是私人的。 决定取决于您要实现的目标。
  • 我们在UsersListViewModel中注入UsersListNavigationDelegate ,以使Router以抽象方式与View Model进行通信。 我们将在Router中看到此委托的详细信息。

查看演示者

该层的名称可能会有些混乱。 我们习惯将 Presenter 称为更新 View 层—在VIPER和MVP中具有此层。 在此体系结构中,演示者称为 View Model 并且该层不是演示者,而是 View 演示者。 继续阅读以了解其责任。

View Presenter是使用4V Engine编写的组件的最后一个难题。

该层负责在设备屏幕中显示组件。

为了实现这个目标,它必须知道显示什么以及在哪里显示。 要添加的View FactoryView Factory提供,并且父View Factory是从外部注入的。

让我们从示例应用程序中查看一个示例:

 final class UsersListViewPresenter: ViewPresenter { 
  private let viewFactory: UsersListViewFactory 
  init(navigationDelegate: UsersListNavigationDelegate) { 
viewFactory = UsersListViewFactory(navigationDelegate: navigationDelegate)
}
  // Method to add the component in a parent view controller 
func present(in parentViewController: UIViewController) {
// Method of UIViewControllerExtension.swift to add a child view controller filling the parent view with
// autolayout.
// Look at UIViewControllerExtension.swift for more details
parentViewController.addFillerChildViewController(viewFactory.viewController)
}
  // Method to remove the component from the device screen 
func remove() {
viewFactory.viewController.view.removeFromSuperview()
viewFactory.viewController.removeFromParentViewController()
}
}

在此示例中, present()是添加子视图控制器的非常简单的方法。 如果您有精美的UIViewController过渡,则此方法是管理过渡的正确位置。

您可能会注意到,我们正在 通过各层 传播 UsersListNavigationDelegate 以便在 View Model 使用它 这是将体系结构分为几层的缺点。

路由器

我们刚刚看完单个组件的各个层。 至此,我们几乎已经准备好在屏幕上显示该组件。 我们需要最后一步:决定何时显示组件。 这是Router的职责。

我们通常每个故事都有一个Router 。 在这种情况下,我对故事的定义是:

这组组件一起在我们的应用程序中定义了流程。

在示例应用程序中,我们有故事“ Users ,这是一组用户列表和用户详细信息。 其他故事可以是:

  • 入门:一组视图以显示如何使用该应用程序。
  • 注册:一组用于创建新帐户,接受使用条款,验证电子邮件等的视图…
  • 购买项目:显示购物篮的一组视图,添加送货地址,添加付款卡明细,…

让我们看看如何在示例应用程序中将Router用于故事Users

 // Delegate used to navigate from users list to user details 
protocol UsersListNavigationDelegate: class {
func usersListSelected(for user: User)
}
 // Delegate used to close the user details 
protocol UserDetailsNavigationDelegate: class {
func userDetailsCloseDidTap()
}
 final class UsersRouter { 
  // Parent view controller to add the components 
fileprivate let parentViewController: UIViewController
  // Dictionary of presenters used 
fileprivate var presenters = [String: ViewPresenter]()

init(parentViewController: UIViewController) {
self.parentViewController = parentViewController
}
}
 extension UsersRouter: Router { 
// Shows first component, the users list
func showInitial() {
let usersListPresenter = UsersListViewPresenter(navigationDelegate: self)
usersListPresenter.present(in: parentViewController)
  presenters["UsersList"] = usersListPresenter 
}
  // Closes the router removing all its components 
func close() {
presenters.keys.forEach { [unowned self] in
self.removePresenter(for: $0)
}
}
  fileprivate func removePresenter(for key: String) { 
let userDetailsPresenter = presenters[key]
userDetailsPresenter?.remove()
  presenters[key] = nil 
}
}
 extension UsersRouter: UsersListNavigationDelegate { 
func usersListSelected(for user: User) {
let userDetailsPresenter = UserDetailsViewPresenter(user: user, navigationDelegate: self)
userDetailsPresenter.present(in: parentViewController)

presenters["UserDetails"] = userDetailsPresenter
}
}
 extension UsersRouter: UserDetailsNavigationDelegate { 
func userDetailsCloseDidTap() {
// Removes user details components from the parent view controller
removePresenter(for: "UserDetails")
}
}

注意:

  • Router有一个与主持人一起使用的字典。 这样,如果我们要删除诸如userDetailsCloseDidTap中的组件,则可以使用键轻松获得合适的演示者。

我们可以看看AppDelegate来了解如何使用路由器:

 @UIApplicationMain 
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow? 
var usersRouter: Router?
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 
  let rootViewController = UIViewController() 

window = UIWindow()
window?.rootViewController = rootViewController

let usersRouter = UsersRouter(parentViewController: rootViewController)
usersRouter.showInitial()

window?.makeKeyAndVisible()

self.usersRouter = usersRouter

return true
}
}

结论

我认为本文是4V Engine 1.0.0版的介绍。 我更改了很多时间,并且我相信仍有改进的空间。 因此,我想对您的意见发表一些意见,我们将不胜感激。 谢谢。