积木法搭建 iOS 应用—— VIPER

Posted 搜狐技术产品

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了积木法搭建 iOS 应用—— VIPER相关的知识,希望对你有一定的参考价值。


点击蓝字关注我们



本文字数:8391

预计阅读时间:23分钟


在我们构建应用产品的时候,产品的快速发展也迫使我们不断寻求更合适产品高速迭代发展的编程架构。

伴随着产品的发展,让产品每一个部分容易被识别,拥有明显特定的目的,并且与其他部分逻辑清晰、结构明确是我们一直探寻的目标。

想必大家已经对经常使用的MVC、MVP、MVVM非常熟悉了,在本文中我们将探索 VIPER 架构在 ios 上的成功实践。


我们先大致了解什么是 VIPER。

VIPER 分为五个部分:View、Interactor、Presenter、Entity、Router。

View:视图部分,根据 Presenter 的要求展示界面。

Interactor:业务相关逻辑、从本地和网络获取数据,并存储数据。

Presenter:包含为显示做准备工作的相关视图逻辑(从 Interactor 接收数据,并进一步处理为 View 可以直接展示的数据),并对用户输入进行反馈(根据用户操作对当前数据做变更)。

Entity:包含 Interactor 要使用的基本模型对象。

Router:包含用来描述屏幕显示 view 和显示顺序的导航逻辑。

这种功能划分形式遵循单一职责原则。Interactor 负责业务分析获取内容的部分,Presenter 代表交互设计师为 View 展示做准备,而 View 相当于视觉设计师只负责展示内容,Entity 负责承载数据内容, Router 负责页面模块的显示和导航逻辑。

我们可以把他们之间关系画为下图:


VIPER 的每一个部分的创建、功能实现没有先后顺序,可以根据实际情况调整。

由于遵循职责单一,每一个部分也都可以拿出来给有相同功能的业务使用,

比如狐友APP中的关注、粉丝页面:




再比如小红书中的发现页面和关注页面:




VIPER 的每一个部分就像是房子的梁、柱、墙以及装修材料,我们可以通过把形状、特点相同的结构重复利用搭建在不同的位置上,从而构建出我们想要的漂亮房子。

这种感觉是不是像极了我们小时候玩积木的样子?

房子维修起来也非常方便。
如果我觉得室内的柱子太单调了,想要所有的柱子都统一换成洛可可风格的柱子,因为柱子都是复用的材料,那么我只需要修改一个柱子的属性,所有的柱子都会变成洛可可风格的样子。


下面我们来写一个推荐电影的列表,根据这个例子更深入的探索如何创建 VIPER 架构应用。

首先,我们针对各个部分的关系和功能定义通用协议,就像拼装日式木质结构的房子需要先有标准部件结构,再将标准部件结构组装起来一样,我们需要先构建 VIPER 的基础构件。

其次,后面我们会用这些基础构件搭建我们需要的业务逻辑。



基础构件

01


Router

Router 用来描述屏幕显示 View 和显示顺序的导航逻辑。在 VIPER 中我们把 viewController 看做是 View 的一部分,只做 view 的显示控制及用户操作反馈,不实际处理数据逻辑。

这里我们定义了可以获取设置 viewController 的属性。

/// Describes router component in a VIPER architecture.
protocol RouterTypeclass 
    /// The reference to the view which the router should use
    /// as a starting point for navigation. Injected by the builder.
    var viewController: UIViewController?  get set 


Interactor

Interactor 它是获取特定的数据并且组织数据的第一步。它与业务逻辑紧密相连,与展示逻辑分离,可以有独立的测试用例,可以较好的使用 TDD(即 Test Driven Development) 进行开发。Interactor 中的工作应当独立于任何显示界面,Interactor 可以同时运用于不同设备类型的数据提供层。
为了保持 Interactor 获取数据部分具体实现时的自由灵活多变,这里我们先不做过多定义。
/// Describes interactor component in a VIPER architecture.
protocol InteractorTypeclass  


Presenter

Presenter 从 Interactor 接收数据做显示准备相关的处理后交给相关视图;并且对用户输入进行反馈,如果需要更新数据时通知 interactor 获取新的数据。

这里我们定义了 InteractorType 类型的 interactor 属性。

/// Describes presenter component in a VIPER architecture.
protocol PresenterTypeclass 
    
    associatedtype IInteractorType
    /// A interactor
    var interactor: I  get   


View

View 根据 Presenter 的要求展示界面,所以我们定义刷新视图的方法以及一个遵守 PresenterType 的 presenter。

protocol ViewType 
    associatedtype PPresenterType
    /// A presenter
    var presenter: P  get 
    
    // MARK: - refresh View
    func refreshView()
    


现在我们已经搭建好了Router、View、Presenter、Interactor之间的单向关系,如下图:



接下来,我们使用协议来完成各个模块之间的数据流动和用户行为反馈。

ListDataProtocol

由于应用程序中大部分页面都是列表,所以我们对列表也做一些通用的功能处理,减少业务层的重复逻辑。

我们的列表数据需要有 row 和 section ,我们需要定义行和组一些显示需要的通用信息:

protocol ViewModelType 
    var cellId: String  get 
    var cellSize: CGSize  get 


protocol SectionType 
    var items: [ViewModelType]  get set 
    
    var headerSize: CGSize  get 
    var footerSize: CGSize  get 
    
    var headerId: String  get 
    var footerId: String  get 
    
    var headerTitle: String  get 
    var footerTitle: String  get 


我们定义了一些 row 和 section 的类型 id、size 以供列表使用。因为在实际业务中 ViewModelType 需要根据业务需求定义不同类型,供不同功能需求使用,但是 SectionType 的功能需求及实现大部分相同,所以我们只定义通用的 section 类型如下:
class SectionSectionType 
    var items: [ViewModelType] = []
    
    var headerSize: CGSize = CGSize.zero
    var footerSize: CGSize = CGSize.zero
    
    var headerId: String = ""
    var footerId: String = ""
    
    var headerTitle: String = ""
    var footerTitle: String = ""


下面我们定义关于列表数据的协议,把上面的 row 和 section 组织起来为列表提供数据支持。这里定义协议包括:列表数据的数组、获取行和组的信息、判断一个 indexPath 是否是有效的。
protocol ListDataProtocolclass 
    // MARK: -
    // MARK: - Data information
    var viewModels: [Section]  get set 
    
    func numberOfSections() -> Int
    func numberOfItemsInSection(at index: Int) -> Int
    
    func item(at indexPath: IndexPath) -> ViewModelType?
    func section(in index: Int) -> Section?
    
    // MARK: -
    // MARK: - legitimacy
    func indexPathAccessibleInViewModels(_ indexPath: IndexPath) -> Bool


indexPathAccessibleInViewModels: 方法接受一个 indexPath ,并且返回这个 indexPath 是否在当前 viewModels 中可以访问的布尔值,以便我们减少重复书写判断数组越界的逻辑。


上面方法的实现通常是相同的,我们写默认实现如下:
  • 获取 row、section 数量:
  • extension ListDataProtocol 
        
        // MARK: -
        // MARK: - Data information
        func numberOfSections() -> Int 
            return self.viewModels.count
        
        
        func numberOfItemsInSection(at index: Int) -> Int 
            #if DEBUG
            assert(index < self.viewModels.count"Index out of bounds exception")
            #else
            #endif
            return section(in: index)?.items.count ?? 0
        

  • 获取 row、section 数据模型:
  • extension ListDataProtocol 
        
        func item(at indexPath: IndexPath) -> ViewModelType? 
            if indexPathAccessibleInViewModels(indexPath) == false 
                return nil
            
            
            return self.viewModels[indexPath.section].items[indexPath.row]
        
        
        func section(in index: Int) -> Section? 
            #if DEBUG
            assert(index < self.viewModels.count"Index out of bounds exception")
            #else
            #endif
            if index >= self.viewModels.count 
                return nil
            
            
            return self.viewModels[index]
        


  • 判断 IndexPath 是否在当前 viewModels 中可以访问:
  • extension ListDataProtocol 
        // MARK: -
        // MARK: - legitimacy
        func indexPathAccessibleInViewModels(_ indexPath: IndexPath) -> Bool 
            
            #if DEBUG
            assert(indexPath.section < self.viewModels.count"Index out of bounds exception (        please check indexPath.section)")
            assert(indexPath.row < self.viewModels[indexPath.section].items.count"Index out of bounds exception (please check indexPath.row)")
            #else
            #endif
            
            if indexPath.section >= self.viewModels.count ||
                indexPath.row >= self.viewModels[indexPath.section].items.count 
                return false
            
            
            return true
        

    由于我们经常需要对数据进行修改更新、数据持久化操作,所以在 ListDataProtocol 中定义数据处理的通用协议及实现如下:
  • 更新row、section 数据:
  • 协议定义:
    protocol ListDataProtocolclass 
        
        // MARK: -
        // MARK: - Data manipulation
        
        /// Retrieve data from memory 
        func updateSection(section: Section, at index: Int)
        func updateItem(item: ViewModelType, at indexPath: IndexPath)


    通常实现相同,添加默认实现如下:

    extension ListDataProtocol 
        
        // MARK: -
        // MARK: - Data manipulation
        
        /// Retrieve data from memory
        func updateSection(section: Section, at index: Int) 
            #if DEBUG
            assert(index < self.viewModels.count"Index out of bounds exception")
            #else
            #endif
            
            if index >= self.viewModels.count 
                return
            
            
            self.viewModels[index] = section
        
        func updateItem(item: ViewModelType, at indexPath: IndexPath) 
            guard indexPathAccessibleInViewModels(indexPath) else 
                return
            
            
            self.viewModels[indexPath.section].items[indexPath.row] = item
        


  • 插入row、section数据:
  • 协议定义:

    protocol ListDataProtocolclass 
        
        /// Insert data
        func insertSection(section: Section, at index: Int)
        func insertItem(item: ViewModelType, at indexPath: IndexPath)


    通常实现相同,添加默认实现如下:
    extension ListDataProtocol 
        
        /// Insert data
        func insertSection(section: Section, at index: Int) 
            #if DEBUG
            assert(index <= self.viewModels.count"Index out of bounds exception")
            #else
            #endif
            
            if index > self.viewModels.count 
                return
            
            
            self.viewModels.insert(section, at: index)
        
        func insertItem(item: ViewModelType, at indexPath: IndexPath) 
            #if DEBUG
            assert(indexPath.section <= self.viewModels.count"Index out of bounds exception (indexPath.section)")
            assert(indexPath.row <= self.viewModels[indexPath.section].items.count"Index out of bounds exception (indexPath.row)")
            #else
            #endif
            
            if indexPath.section > self.viewModels.count ||
                indexPath.row > self.viewModels[indexPath.section].items.count 
                return
            
            self.viewModels[indexPath.section].items.insert(item, at: indexPath.row)
        


  • 删除row、section数据:
  • 协议定义:

    protocol ListDataProtocolclass 
         
        /// Delete data
        func deleteSection(at index: Int)
        func deleteItem(at indexPath: IndexPath)


    通常实现相同,默认实现如下:

    extension ListDataProtocol 
        
        /// Delete data
        func deleteSection(at index: Int) 
            #if DEBUG
            assert(index < self.viewModels.count"Index out of bounds exception")
            #else
            #endif
            
            if index >= self.viewModels.count 
                return
            
            
            self.viewModels.remove(at: index)
        
        func deleteItem(at indexPath: IndexPath) 
            guard indexPathAccessibleInViewModels(indexPath) else 
                return
            
            
            self.viewModels[indexPath.section].items.remove(at: indexPath.row)
        


  • 清空当前列表数据:
  • // 协议定义
    protocol ListDataProtocolclass 
        /// Clear all data
        func clearList()


    // 协议实现
    extension ListDataProtocol 
        /// Clear all data
        func clearList() 
            self.viewModels = []
        
    除此之外还有数据库的数据增删改查操作等等,此处不一一列举实现。


    ListViewProtocol

    列表的 view 通常需要注册,列表需要有下拉刷新、上拉加载等功能,我们定义列表 view 的协议如下:
    protocol ListViewProtocol 
        
        // MARK: - load
        func pulldown()
        func loadMore()
        
        // MARK: - register
        func registerCellClass() -> [AnyClass]
        func registerCellNib() -> [AnyClass]
        func registerHeaderClass() -> [AnyClass]
        func registerHeaderNib() -> [AnyClass]
        func registerFooterClass() -> [AnyClass]
        func registerFooterNib() -> [AnyClass]
        
        // MARK: - refresh
        func setUpRefreshHeader()
        func setUpRefreshFooter()

    列表的数据是由协议类型 ListDataProtocol 提供,UICollectionView 及 UITableView 的数据代理方法不能写在有泛型的协议中实现,所以我们需要一个实现含有 UICollectionView 或者 UITableView 属性的类。

    它就是我们上面提到的 ViewType 协议类型,充当 VIPER 中 view 的角色。

    现在我们完成了VIPER 中 View 根据用户操作向 Presenter 索要数据,Presenter 向 view提供显示所需的数据支持,我们需要一个列表 View 去显示 Presenter提供的数据,这就是我们接下来讲的 VTableViewController。


    VTableViewController

    下面我们实现拥有 UITableView 的 Controller。Controller 从 presenter 获取展示需要的数据直接展示在界面上。

    VTableViewController 的 presenter 为视图提供数据的支持,presenter 遵守 PresenterType & ListDataProtocol 两个协议。为了业务层灵活实现 tableView,这里 tableView 是一个泛型:

    /// Viper view controller base class.
    typealias ListPresenterType = PresenterType & ListDataProtocol

    class VTableViewController<PListPresenterTypeTUITableView>: UIViewControllerViewType 
        
        let presenter: P
        
        init(presenter: P, style: UITableView.Style
            self.presenter = presenter
            self.tableView = T.init(frame: CGRect.zero, style: style)
            super.init(nibName: nil, bundle: nil)
            
            self.view.backgroundColor = UIColor.white
        
        
        // MARK: -
        // MARK: - View life cycle
        override func viewDidLoad() 
            super.viewDidLoad()
            hy_setUpUI()
        
        
        // MARK: -
        // MARK: - tableView
        var tableView: T
        private func hy_setUpUI() 
            self.view.addSubview(self.tableView)
            self.tableView.frame = self.view.bounds
            self.tableView.dataSource = self
            self.tableView.delegate = self
        
        
        // MARK: -
        // MARK: - viewType
        func refreshView() 
            self.tableView.reloadData()
        
       

    VTableViewController 需要实现 ListViewProtocol 提供视图刷新的方法,具体刷新的功能需要根据业务层的具体需求实现,所以我们在抽象类只增加空实现,如下:
    class VTableViewController<PListPresenterTypeTUITableView>: UIViewControllerViewTypeListViewProtocol    
        // MARK: -
        // MARK: - ListViewProtocol
        func pulldown() 
        func loadMore() 
        
        func setUpRefreshHeader() 
        func setUpRefreshFooter() 


    具体业务中还需要实现注册视图的方法,在 VTableViewController 中我们只增加空实现,如下:

    class VTableViewController<PListPresenterTypeTUITableView>: UIViewControllerViewTypeListViewProtocol    
        // MARK: -
        // MARK: - ListViewProtocol
        func registerCellClass() -> [AnyClass]  return [] 
        func registerCellNib() -> [AnyClass]  return [] 
        func registerHeaderClass() -> [AnyClass]  return [] 
        func registerHeaderNib() -> [AnyClass]  return [] 
        func registerFooterClass() -> [AnyClass]  return [] 
        func registerFooterNib() -> [AnyClass]  return [] 


    我们需要根据上面注册类型方法返回类型对 VTableViewController 的 tableView 进行注册视图,实现如下:

    class VTableViewController<PListPresenterTypeTUITableView>: UIViewControllerViewTypeListViewProtocol  
    // MARK: - private
        private func hy_registeCell() 
            for cellClass in self.registerCellClass() 
                self.tableView.register(cellClass, forCellReuseIdentifier: NSStringFromClass(cellClass))
            
            
            for cellClass in self.registerCellNib() 
                self.tableView.register(UINib.init(nibName: NSStringFromClass(cellClass), bundle: nil), forCellReuseIdentifier: NSStringFromClass(cellClass))
            
        
        
        private func hy_registeHeaderAndFooterView() 
            
            let headerAndFooterClass = self.registerHeaderClass() + self.registerFooterClass()
            for viewClass in headerAndFooterClass 
                self.tableView.register(viewClass, forHeaderFooterViewReuseIdentifier: NSStringFromClass(viewClass))
            
            
            let headerAndFooterNib = self.registerHeaderNib() + self.registerFooterNib()
            for viewClass in headerAndFooterNib 
                self.tableView.register(UINib.init(nibName: NSStringFromClass(viewClass), bundle: nil), forHeaderFooterViewReuseIdentifier: NSStringFromClass(viewClass))
            
        


    我们需要在tableView创建之后注册复用view,所以需要更改前面 hy_setUpUI 方法为:

    private func hy_setUpUI() 
            self.view.addSubview(self.tableView)
            self.tableView.frame = self.view.bounds
            self.tableView.dataSource = self
            self.tableView.delegate = self
            self.hy_registeCell()
            self.hy_registeHeaderAndFooterView()

    VTableViewController 需要根据 presenter 提供的数据显示列表视图部分,我们需要实现 UITableViewDelegate, UITableViewDataSource 两个协议,这个时候我们就需要用到 presenter 在 PresenterType 和 ListDataProtocol中定义的方法,从 presenter 中直接拿到可以用来展示的数据给视图展示。

    我们接下来添加 UITableViewDataSource 相关的 cell 显示方法实现:

    class VTableViewController<PListPresenterTypeTUITableView>: UIViewControllerViewTypeListViewProtocolUITableViewDelegateUITableViewDataSource    
        // MARK: -
        // MARK: - tableView data source
        func numberOfSections(in tableView: UITableView) -> Int 
            return self.presenter.numberOfSections()
        
        
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int 
            return self.presenter.numberOfItemsInSection(at: section)
        
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 
            
            let cellId = self.presenter.item(at: indexPath)?.cellId ?? ""
            
            #if DEBUG
            assert(self.presenter.item(at: indexPath) != nil"There is no item")
            assert(cellId.isEmpty != true"Item don\'t has cellId")
            #else
            if cellId.isEmpty 
                return UITableViewCell.init()
            
            #endif
            
            guard let cell = tableView.dequeueReusableCell(withIdentifier: cellId) else 
                return UITableViewCell.init()
            
            
            return cell
        

    通过上面的代码我们可以将 presenter 中已经准备好的数据交给 tableView 显示。

    通常列表中除了 cell 的显示还有 sectionHeader、sectionFooter 的显示,我们依然通过 presenter 给的数据来显示这些视图:

     class VTableViewController<PListPresenterTypeTUITableView>: UIViewControllerViewTypeListViewProtocolUITableViewDelegateUITableViewDataSource 
        // MARK: -
        // MARK: - tableView data source
        func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? 
            #if DEBUG
            assert(self.presenter.section(in: section) != nil"There is no section")
            #endif
            return self.presenter.section(in: section)?.headerTitle ?? ""
        
        
        func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? 
            #if DEBUG
            assert(self.presenter.section(in: section) != nil"There is no section")
            #endif
            return self.presenter.section(in: section)?.footerTitle ?? ""
        
       
       // MARK: -
       // MARK: - tableView delegate
        func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? 
            #if DEBUG
            assert(self.presenter.section(in: section) != nil"There is no section")
            #endif
            let headerId = self.presenter.section(in: section)?.headerId ?? ""
            
            // No found header
            guard let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: headerId) else 
                return nil
            
            
            return header
        
        
        func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? 
            #if DEBUG
            assert(self.presenter.section(in: section) != nil"There is no section")
            #endif
            let footerId = self.presenter.section(in: section)?.footerId ?? ""
            
            // No found header
            guard let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: footerId) else 
                return nil
            
            
            return header
        
     

    cell、sectionHeader、sectionFooter 还需要设置大小。

    这里我们默认所有 view 都会被注册,在 release 版本中对于不能获取到复用 Id 的视图 size 将被设置为 0 ,它将不展示给用户。在 debug 版本中,我们将依然会展示此 View 以便及时发现问题,并更正错误。

    所以现实代理方法如下:

    class VTableViewController<PListPresenterTypeTUITableView>: UIViewControllerViewTypeListViewProtocolUITableViewDelegateUITableViewDataSource   
        
        // MARK: -
        // MARK: - tableView delegate
        func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat 
            #if DEBUG
            assert(self.presenter.item(at: indexPath) != nil"There is no item")
            return self.presenter.item(at: indexPath)?.cellSize.height ?? 0
            #else
            
            guard let cellId = self.presenter.item(at: indexPath)?.cellId else 
                return 0
            
            
            if cellId.isEmpty 
                return 0
            
            
            // No found cell
            let registeCells = registerCellClass() + registerCellNib()
            guard (registeCells.contains  NSStringFromClass($0) == cellId) else 
                return 0
            
            return self.presenter.item(at: indexPath)?.cellSize.height ?? 0
            #endif
        
        
        func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat 
            #if DEBUG
            assert(self.presenter.section(in: section) != nil"There is no section")
            return self.presenter.section(in: section)?.headerSize.height ?? 0
            #else
            // There is no headerId
            guard let headerId = self.presenter.section(in: section)?.headerId else 
                return 0
            
            if headerId.isEmpty 
                return 0
            
            
            // No found header
            let registeHeaders = registerHeaderClass() + registerHeaderNib()
            guard (registeHeaders.contains  NSStringFromClass($0) == headerId) else 
                return 0
            
            return self.presenter.section(in: section)?.headerSize.height ?? 0
            #endif
        
        
        func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat 
            #if DEBUG
            assert(self.presenter.section(in: section) != nil"There is no section")
            return self.presenter.section(in: section)?.footerSize.height ?? 0
            #else
            guard let footerId = self.presenter.section(in: section)?.footerId else 
                return 0
            
            if footerId.isEmpty 
                return 0
            
            
            // No found footer
            let registeFooters = registerFooterClass() + registerFooterNib()
            guard (registeFooters.contains  NSStringFromClass($0) == footerId) else 
                return 0
            
            return self.presenter.section(in: section)?.footerSize.height ?? 0
            #endif
        


    我们先定义一个 tableViewCell 的基类。
    这里 cell 没有用协议而是定义了基类是因为 viewModel 是一个泛型类型,使用协议业务层 tableView 的代理方法中会增加很多重复的代码,这里使用基类更方便。
    class HYTableViewCell<T>: UITableViewCell 
        var viewModel: T?
        func setViewModel(_ viewModel: T) 
            self.viewModel = viewModel
        


    这样我们就已经写好了 tableView 需要显示的基本内容。
    业务层可以用 VTableViewController 快速开始 VIPER 之旅。
    这里需要注意的是由于 VTableViewController 是带泛型的协议类型,所以如果子类要调用 tableView 的 UITableViewDelegate 方法和 UITableViewDataSource 方法,父类必须写子类需要调用方法的空实现,否则子类的方法不会被调用。

    iOS VIPER架构

    洋葱模型
      洋葱模型,是从冰山模型上演变而来的,用来进行层次分析的模型,这是Redux的洋葱模型。


      action从最外层传入,层层传递直至核心后,经过逐层事件触发,再次被分发出来,执行后续操作。

      洋葱模型如今已经广泛应用于各个领域,进行更直观清晰的分层剖析。

    The Clean Architecture
      Robert C·Martin是《Clean Code》的作者,我们习惯称他为Uncle Bob。2012年8月13日,他在他的个人Blog上,提出了著名的The Clean Architecture。

     

      Uncle Bob利用洋葱模型,设计出了The Clean Architecture。

      这种架构的核心在于依赖的规则:源码依赖只能指向内部,内圈的部分,并不知道关于外圈的任何事情,即内部不依赖外部,外部依赖于内部。一般来说,越深入则代表软件级别越高,外部是机制,而内部是策略。

      The Clean Architecture可以给软件带来如下优点:

    独立的框架:框架可作为工具,不与系统混在一起;
    可测试:没有UI、数据库和Web服务器,数据也可以测试;
    UI独立:UI改变更容易,而不会影响业务规则;
    数据库独立:业务规则不会和数据库绑定,可以任意切换数据库;
    外部代理独立:业务规则可以对外部世界无感知。

      The Clean Architecture也符合领域驱动设计DDD(Domain Driven Design)的思想,代码即方案。DDD是30年来业务软件开发实践经验的结晶。在传统编码模式中,如MVC架构,久而久之会演变为Massive View Controller,即重量级视图控制器,代码也将变为“面条代码”,而后面所有维护的人员,都将陷入耦合和臃肿的代码泥潭中,被代码所蛊惑,无法自拔,重心离要解决的问题越来越远,越走越偏,所以我们更需要使用模型来交流。

      DDD希望,即使不是开发人员,在参与开发的各方,也都能理解软件模型。

      在The Clean Architecture中,将软件分为四个层次,即Entities、Use Cases、Interface Adapters、Frameworks and Drivers。

    Entities:Enterprise Business Rules,业务实体;

    Use Cases:Application Business Rules,封装和实现业务功能,负责实体的数据处理转换,与数据库和UI无关;
    Interface Adapters:接口适配器层,如MVC中的Controllers,MVP中的Presenters;
    Frameworks and Drivers:由UI、数据库框架、Web框架等框架和工具构成。

      这种由外向内的结构在改进的MVP架构中,可以表现为View -> Presenter -> Interactor -> Entity,通过这些原则将系统进行分层,将会创建一个可测试的架构体系。

      现在,The Clean Architecture已经成为了开发界的Vans。

      这是2016年,在Android端实践过The Clean Architecture的开发者Dario Mili?i?的感受。

    For me, this has been the best way to develop apps so far. Decoupled code makes it easy to focus your attention on specific issues without a lot of bloatware getting in the way. After all, I think this is a pretty SOLID approach but it does take some time getting used to.

      他说,”对于我来说,这是目前最好的App开发方式。解耦的代码使你更容易将注意力集中在具体问题上,而不是被臃肿的代码所妨碍。我认为这是一个相当可靠的方法,但它需要一段时间去适应。”

    MVP-Clean
    Android Architecture Blueprints
      The Clean Architecture也是MVP和MVVM都在追求的结构。

      Google官方发布了安卓架构蓝图Android Architecture Blueprints,其中包括MVP + Clean Architecture,在MVP-Clean中,其在传统的MVP上,基于The Clean Architecture,对MVP做了改进。

      MVP起源于20世纪90年代,最初用于C++底层编程,后来被迁移到Java中进而推广,旨在促进自动化单元测试。这是安卓架构蓝图的MVP模型,拥有基本的Model-View-Presenter结构。

     

      经过Clean改进的安卓架构蓝图MVP,其洋葱模型如下所示:

     

      它在UI层和存储层添加了Domain,将应用分为三层,如下图所示:

     

      Domain层负责提供业务逻辑,Domain层和Use Cases上很完美的解决了代码重复的问题,即相同的业务逻辑可以使用Use Case进行复用。

      Domain层的所有代码也可以进行单元测试,也可以扩展使用集成测试,覆盖了一些数据边界后,MVP-Clean架构可以很快地为App提供良好的自动化测试基础。

      通过Domain层的Use Cases,模块的职能一目了然,这是符合DDD的思想的,即使是其他非开发人员参与到项目中,通过Use Cases也能快速清晰地明白每个模块都负责什么样的业务,拥有什么样的功能。

      安卓架构蓝图中的Use Cases提供一个CallBack,包括数据处理成功和处理失败的回调,而Presenter负责根据回调后的结果,处理View的界面显示,Presenter控制器不再单一的控制View和Model,承担过重的业务逻辑,而相同的业务逻辑不能复用。MVP-Clean的Presenter因为有了Use Cases,使得自身更加干净,分离出来的业务逻辑和数据处理等由Use Cases承担,其他业务也能够良好复用。

    VIPER
      VIPER架构在2013年由Jeff Gilbert 和 Conrad Stoll 提出。其最初是为了解决测试编写的难题。因为MVC演变的Massive View Controller的结构,使得Controllers越来越大,而将业务逻辑纯粹转移到Presenter也使得Presenter担任同样过重的职责,业务逻辑也不能够很好的复用。VIPER是在一定程度上基于The Clean Architecture的思想后改进而来的方案。

      使用VIPER架构改进,具有以下优点:

    易于测试;
    代码结构更清晰,符合DDD;
    良好的分离关注点;
    责任被划分的很清晰:做什么和怎么做。

      VIPER中各个字母分别代表View、Interactor、Presenter、Entity和Router。

      VIPER符合单一职责原则,Interactor负责控制业务逻辑,Presenter负责控制Interactor和View的交互,View负责设计展示UI。Presenter定义行为,而Interactor定义业务逻辑,各个组件连接结构关系可以用如下图表示:

     

      这张图清晰表达了数据流和依赖流。View负责通知Presenter生命周期,Presenter向Interactor发送数据,请求数据处理,Interactor知道应该如何处理Entity,Presenter负责更新UI,而WireFrame负责跳转。

     

    Interactor
      Interactor层中主要包含各个业务中的Use Case,不包含任何UI相关的操作,这些Use Case通过Callback回调给Presenter状态,success或error,并传递响应的处理结果。

      因为Interactor更纯粹,只涉及数据操作,所以其对TDD(测试驱动开发)更加友好。


    Entity
      实体负责定义数据结构,这里的实体并不像其他经典著作中,需要定义更多的数据处理的行为,在VIPER中,数据处理更多是由Interactor来完成,而Entity更加纯粹。

     

    Presenter
      Presenter承担的是When to do的职责。知道什么时候通知Interactor处理数据,知道什么时候通知View更新UI显示,也知道什么时候应该进行跳转。

     

    Router
      Router负责链接各个模块,Presenter知道何时跳转却不知道如何跳转,Router则承担How to do的职责进行各个模块之间的交互转向。

     

    View
      View负责展示界面,但不知自己应该何时展示,Presenter控制View的展示时机,View只负责绘制自身的表现,而将接口暴露出来,让Presenter控制其行为。

     

    总结
      从臭名昭著的MVC,到MVVM,数据绑定对测试和调试并不友好,而代码复用也存在问题,再到今天过重Presenter的MVP,代码复用仍然存在问题,而Presenter的职责也使得开发人员的重心过多沉溺于如何重构代码之上,甚至久而久之就演变为破窗理论,这些结构都不能够很完美的被接受。

      The Clean Architecture是改善测试、代码混乱和代码复用问题的明星理论,众多权威人士都对其拥有极高的认可度,而VIPER在IOS系统已获得了更多的认可,VIPER也是对The Clean Architecture的比较美的诠释。

      VIPER和MVP-Clean还是有非常多的相似点,MVP-Clean更像是VIPE架构,而缺少了一层Router。

      如果对于组件化来说,Router这层是必不可少的,而MVP-Clean解决了代码复用和测试等问题,VIPER则将二者巧妙并自然地衔接起来,安静地谱写着一篇美妙的乐章。(本章节图片来自网络)

    以上是关于积木法搭建 iOS 应用—— VIPER的主要内容,如果未能解决你的问题,请参考以下文章

    iOS VIPER架构

    Viper的简单实用

    viper读取配置文件

    viper读取配置文件

    viper读取配置文件

    iOS架构模式--解密 MVC,MVP,MVVM以及VIPER架构