IOS架构模式MVC、MVP、MVVM

软件架构的定义?它的存在意义是什么?

软件架构是一系列相关的抽象模式,用于指导软件系统各个方面的设计。软件架构描述的对象是直接构成系统的抽象组件。各个组件之间的连接则明确和相对细致地描述组件之间的通讯。在实现阶段,这些抽象组件被细化为实际的组件,比如具体某个类或者对象。在面向对象领域中,组件之间的连接通常用接口来实现。

良好的软件架构具有许多的优点:可靠性、安全性、可扩展性、可维护性

软件开发中存在这许多已经相当成熟的和广泛应用的架构模式MVC、MVP、MVVM、VIPER等

什么是MVC

MVC全名是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。

M:Model(模型)- 业务数据存储

V:View(视图) - 数据展示以及和用户进行交互

C:Controller (控制器)- 协调Model和View。处理View上的操作,进行相应业务处理,并将结果数据更新到Model上,同时将更改的信息返回到View上展示。

MVC优点?缺点?

优点:

1、低耦合性

视图层和业务层分离,这样就允许更改视图层代码而不用重新编译模型和控制器代码,同样,一个应用的业务流程或者业务规则的改变只需要改动MVC的模型层即可。因为模型与控制器和视图相分离,所以很容易改变应用程序的数据层和业务规则

2、高易用性、可适用性和复用性

其高度的简易性,铸就了其高度的广泛适用性。分离了业务逻辑的数据模型和视图模型又可重复利用。

缺点:

1、职责划分无法保证

在传统的MVC中,MVC更倾向于一种Observer, Composite和Strategy组成的模式,如下

image

View和model是绑定在一起的,这为View绕过Controller而直接对Model进行操作提供了便利,而这样做的结果就是用做展示和交互职责的View不可避免的承担了部分业务逻辑,从而导致了View的职责模糊化。而这部分逻辑又是View基于Model定制的,进而导致了View和Model的耦合性增强。至此Model、View、Controller三者相互都有通信,紧密耦合,不可避免的大大降低了三者的复用性。

    import UIKit
    
    class Person {
        let name: String
        var greeting: String?
        
        init(_ name: String, greetingText greeting: String?) {
            self.name = name
            self.greeting = greeting
        }
    }
    
    class TableViewController: UITableViewController {
        
        var person: Person!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            person = Person("Tom", greetingText: "Hello word")
            tableView.reloadData()
        }
        
        override func numberOfSections(in tableView: UITableView) -> Int {
            return 1
        }
        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return 1
        }
        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "inputCellReuseId", for: indexPath) as! TableViewInputCell
            cell.delegate = self
            cell.textfield.text = person.greeting
            return cell
        }
    }
    
    extension TableViewController: TableViewInputCellDelegate{
        func inputChange(cell: TableViewInputCell, text: String) {
            title = person.name + " " + (person.greeting ?? " ")
        }
    }
    
    protocol TableViewInputCellDelegate: class {
        func inputChange(cell: TableViewInputCell, text: String)
    }
    
    class TableViewInputCell: UITableViewCell, UITextFieldDelegate {
        
        @IBOutlet weak var textfield: UITextField!
        weak var delegate: TableViewInputCellDelegate?
            
        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            person?.greeting = textField.text
            delegate?.inputChange(cell: self, text: textField.text ?? "")
            return true
        }
    }

以上是个简单的例子,当输入变化时,title随之变化。

cell中,当输入变化时直接修改了person中greeting字符串,这就使得cell包含了输入所对应的部分业务,从而导致view的职责模糊化,不单单只是展示数据。

2、维护性差

Controller作为承担胶水代码和业务逻辑部分,开发者将大量代码扔到其中用于协调View和Model,View的最大的任务就是向Controller传递用户动作事件,Controller最终会承担一切代理和数据源的职责,还负责一些分发和取消网络请求以及一些其他的任务,在Controller规模尚小时没有太大问题,但随着版本和业务的迭代,造成Controller逻辑复杂,持续这样下去必定将导致Model View Controller 变成Massive View Controller,代码一天天的烂下去,直到没人敢碰。

苹果开发人员认为View和Model应该是最有效的可复用的部分苹果看MVC,因而传统的MVC并不能满足苹果的期望

苹果期望的MVC运行模式模型如下:


1.png

现在基于Cocoa MVC 对以上的例子做修改

    extension TableViewController: TableViewInputCellDelegate{
        func inputChange(cell: TableViewInputCell, text: String) {
            person?.greeting = text
            title = person.name + " " + (person.greeting ?? " ")
        }
    }
    
    class TableViewInputCell: UITableViewCell, UITextFieldDelegate {
        
        //. . .
        
        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            delegate?.inputChange(cell: self, text: textField.text ?? "")
            return true
        }
    }

此时cell内部不在对person数据进行操作,它将用户动作事件传递给delegate即Controller,由其进行相应业务处理。

而MVP架构模式与此十分相似

3、难以测试

Controller混杂了各种视图处理逻辑和业务逻辑,且分散在不同的地方,分离这些成分的单元测试变为一个艰巨的任务


MVC的优化MVP解决的什么问题?

M:Model(模型)- 业务数据存储

V:View / Controller(视图) - 数据展示以及和用户进行交互

P:Presenter (协调器)- 协调Model和View。接受View传递的交互事件,进行相应业务处理,并将结果数据更新到Model上,最后对View进行更新操作。


2.png

对比MVC,在MVP中Presenter替代了Controller的作用,由Presenter进行数据源操作,并更新View的数据和状态,以及其他业务逻辑。它将所有model相关的任务(包括更新model,观察变更,将model变形为可以显示的形式等)从controller层抽离出来,放到Presenter对象中,并在数据更新后调用view的更新接口。

创建Presenter,提取Model处理逻辑和View状态更新代码

    protocol GreetingView: class {
        func setGreeting(_ greeting: String)
    }
    
    protocol GreetingPresenterCompatible {
        init(_ view: GreetingView, person: Person)
        func showGreeting()
    }
    
    class GreetingPresenter: GreetingPresenterCompatible {
        
        unowned let view: GreetingView
        let person: Person
        
        var greetingText: String? {
            return person.greeting
        }
        
        required init(_ view: GreetingView, person: Person) {
            self.view = view
            self.person = person
        }
        func changeGreeting(_ text: String?) {
            person.greeting = text
            showGreeting()
        }
        func showGreeting() {
            // 对View进行相应的操作
        }
    }

如此,对model的数据操作就完全包含在了Presenter中,再来看看TableViewController的变化

    class TableViewController: UITableViewController {
        
        var presenter: GreetingPresenter!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            let person = Person("Tom", greetingText: "Hello word")
            presenter = GreetingPresenter(self, person: person)
            tableView.()
        }
        
        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            // ……
            cell.textfield.text = presenter.greetingText
            return cell
        }
    }
    
    extension TableViewController: TableViewInputCellDelegate{
        func inputChange(cell: TableViewInputCell, text: String) {
            presenter.changeGreeting(text)
        }
    }

对model的数据源操作后,需要更新View,补全GreetingPresenter中对View处理函数:

    func showGreeting() {
            let greeting = person.name + " " + (person.greeting ?? " ")
            self.view.setGreeting(greeting)
}

TableViewController进行数据展示:

extension TableViewController: GreetingView {
    func setGreeting(_ greeting: String) {
        title = greeting
    }
}

这样一来,Presenter就完成了M、V、C三者的协调整合。

MVP的好处?

1、剥离了Controller的逻辑处理代码,实现了减负作用。

2、View不直接接触Model的数据,而由Presenting间接获取,隐藏了数据,降低了View和Model的耦合性,提高维护性和可复用性

3、利于测试驱动开发

MVP的弊端?

1、一个Presenter只能持有一个View,而在一个Controller中可能存在多个Presenter,这就不可避免的导致代码量的增加。

2、对视图的更新操作放在Presenter中,导致View和Presenter的交互过于频繁,继而使得Presenter与特定试图的联系过于紧密,一旦视图需要变更,那么Presenter也需要变更了


MVC的最终优化MVVM。

M:Model(模型)- 业务数据存储

V:View / Controller(视图) - 数据展示以及和用户进行交互

VM:ViewModel(视图模型)- 协调Model和View。处理View上的操作,进行相应业务处理,并将结果数据更新到Model上,同时将信息变化回馈给View,由View进行相应的数据展示。

某种意义上讲MVVM和MVP非常类似:

将View和Controller作为View层级;

将所有model相关的任务(包括更新model,观察变更,将model变形为可以显示的形式等)从controller层抽离;

解耦View和Model的联系,协调Model和View;

区别在于:

相比Presenter,VM不再持有View,对View的更新操作交由View自身处理。当数据发生变化时,View-Model回馈给View一个数据产生变化的信号,而view在接收到这个信号后将执行与信号绑定的相关更新操作。因此进一步解耦View和View-Model,明确View-Model自身的职责。
MVVM模型图:


3.png

现在让我们MVVM模式下代码是如何实现的
创建ViewModel,存储Model,以及给定参数修改的函数,它叫做GreetingViewModel:

class GreetingViewModel {
    private var person: Person
    
    var subscribe: ((_ greeting: Person) -> Void)?

    // 提供函数进行相应的逻辑操作,这里只是修改greeting,就简单操作了
    var greeting: String? {
        get {
            return person.greeting
        }
        set {
            guard newValue != person.greeting else {
                return
            }
            person.greeting = newValue
            subscribe?(person)
        }
    }
    
    init(person: Person) {
        self.person = person
    }
    // 订阅数据的变化
    func subscribe(_ handler: @escaping (Person) -> Void) {
        self.subscribe = handler
    }
    
    func unSubcribe() {
        self.subscribe = nil
    }

    var sectionNums: Int {
        return 1;
     }
        
    var sectionRows: Int {
        return 1;
    }
    
}

在 TableViewController 增加一个 GreetingViewModel 变量,并初始化它:

class MVVMTableViewController: UITableViewController {

    var viewModel: GreetingViewModel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let person = Person(name: "Hello", greeting: nil)
        viewModel = GreetingViewModel(person: person)
        
        // 订阅viewModel,每次 viewModel 状态改变时,greetingDidChange 都将被调用
        viewModel.subscribe { [weak self] person in
            self?.greetingDidChange(person: person)
        }
    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        return viewModel.sectionNums
    }
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.rowNums
    }

    func greetingDidChange(person: Person) {
        title = person.name + " " + (person.greeting ?? " ")
    }

    // ...

}

extension MVVMTableViewController: TableViewInputCellDelegate {
    func inputChange(cell: TableViewInputCell, text: String) {
        viewModel.greeting = text
    }
}

MVVM优点:

1、双向绑定技术
当model放生变化时,管理model的View-Model会自自动更新,继而产生数据变化信号令View自动变化,很好的做到了数据的一致性。

2、为Controller实现了减负瘦身功能
将所有model相关的任务(包括更新model,观察变更,将model变形为可以显示的形式等)从controller层抽离出来,放到view-model中

3、方便测试、易维护性
解耦View和Model的联系并抽离业务逻辑类view-model,view无需关系实际的业务逻辑实现,而只承担视图展示和交互传递,易测试修改。而承担业务逻辑的view-model在屏蔽了业务逻辑的同时同样无需关系视图展示易测试。至此其可测试性侧面说明了其易可维护行。

缺点:

不可避免的导致类的增加,以及项目构造的复杂性和代码量增加

实际开发中MVVM经常和一个FRP响应式框架结合使用(例如ReactiveCocoa 或者 ReSwift),快速的将信号和与之相关的操作绑定。


总结:

不论使用哪种软件设计架构,本质都是为了更清晰地管理"用户操作,模型变更,UI反馈"这一数据流动的方式,做到:低耦合,职责明确,提高可复用行、可维护性,可扩展性,而不仅仅只是简单的将实现代码换一个位置存放。

个人能力有限,若有错误还望指出。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,928评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,748评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,282评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,065评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,101评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,855评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,521评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,414评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,931评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,053评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,191评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,873评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,529评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,074评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,188评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,491评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,173评论 2 357

推荐阅读更多精彩内容