iOS架構(gòu)設(shè)計(jì)

作為iOS開發(fā)者應(yīng)該聽到過MVC埋虹,可能在考慮要不要轉(zhuǎn)到MVVM,或者聽說了VIPER這種高大上的,在想采用這種復(fù)雜的架構(gòu)值不值帕识?

本文試圖回答以上問題泛粹,幫助大家對iOS架構(gòu)有一個初步的了解。我們將通過一些簡單的例子介紹架構(gòu)的演進(jìn)肮疗,他們的異同晶姊。

為什么要考慮架構(gòu)選擇的問題

因?yàn)殚_發(fā)時如果不采用架構(gòu),隨著App復(fù)雜度的提高伪货,勢必出現(xiàn)一些巨大的類们衙,在其中定位及修復(fù)bug都會變得越來越困難。代碼組織很可能會是這樣的:

  • 那些巨大的類是UIViewController的子類
  • 在UIViewController中操作數(shù)據(jù)
  • UIView基本不干啥
  • Model只是些數(shù)據(jù)結(jié)構(gòu)碱呼,沒有動作
  • 單元測試并沒有覆蓋到什么

讓人更加郁悶的是砍艾,你明明是按照Apple的推薦來組織代碼的,用的就是Apple的MVC架構(gòu)巍举。不奇怪脆荷,Apple的MVC是有問題的。

好的架構(gòu)需要什么特性

  1. 不同模塊角色明晰懊悯,代碼均衡分布于這些模塊上
  2. 由第1點(diǎn)帶來的可測試性
  3. 易用性蜓谋,維護(hù)成本低

隨著App復(fù)雜度的提高,終會達(dá)到大腦清晰思考的極限炭分。解決之道就是拆分成多個組件桃焕,遵循single responsibility principle,每個組件只完成一個功能捧毛,而這個功能完全封裝在這個組件中观堂。

可測試性對于已經(jīng)嘗到單元測試甜頭的開發(fā)者來說是理所當(dāng)然的,尤其是在增加新特性或者重構(gòu)之后發(fā)現(xiàn)測試通不過的時候呀忧。測試可以提前發(fā)現(xiàn)在運(yùn)行時會出現(xiàn)的問題师痕,設(shè)想這些問題會在用戶使用時發(fā)生,而修復(fù)要在審核周期后才能上線而账。

易用性很好理解胰坟。我們說寫的代碼越少,bug就越少泞辐。所以追求代碼量少笔横,絕不是因?yàn)殚_發(fā)者懶。同時也要避免那些會提高維護(hù)成本的奇技淫巧咐吼。

常見架構(gòu)

  • MVC
  • MVVM
  • VIPER

前兩者結(jié)構(gòu)類似吹缔,都是把App的模塊分成3個大類:

Models:負(fù)責(zé)數(shù)據(jù)或者操作數(shù)據(jù)的數(shù)據(jù)存取層。例如User或者UserDataProvider類锯茄。

Views:負(fù)責(zé)展示層(GUI)厢塘。對iOS來說,包括所有那些前綴是UI的東西。

Controller / ViewModel:Model俗冻,View之間的膠水或者中間人礁叔。對用戶在View所做的操作進(jìn)行響應(yīng),改變Model迄薄,同時當(dāng)Model變化時琅关,更新View。

把模塊分開的好處:

  • 更容易理解
  • 利于重用讥蔽,尤其是View和Model
  • 便于隔離開進(jìn)行測試

MVC

期望

Cocoa MVC

Controller是溝通View和Model的中間人涣易,View和Model之間相互是不知道的。其中重用性最低的是Controller冶伞,這并不是個問題新症,畢竟,總要有個地方放置業(yè)務(wù)邏輯响禽。

這個結(jié)構(gòu)看上去很容易理解徒爹。但現(xiàn)實(shí)的情況是View Controller會變的非常臃腫,給View Controller減肥成為開發(fā)者的一個重要課題芋类。這是怎么發(fā)生的呢隆嗅?

現(xiàn)實(shí)

Realistic Cocoa MVC

Cocoa MVC事實(shí)上鼓勵你寫臃腫的view controller,他們與view生命周期耦合的如此緊密侯繁,以致很難說他們是分開的胖喳。盡管你仍然可以把一些業(yè)務(wù)邏輯,數(shù)據(jù)轉(zhuǎn)化遷移到Model贮竟,想把一些工作轉(zhuǎn)移給view就沒那么容易了丽焊。大多數(shù)時間,所有view的工作就是發(fā)送動作給controller咕别。View controller最后成為各種代理和數(shù)據(jù)源的集中地技健,同時還要負(fù)責(zé)發(fā)起和取消網(wǎng)絡(luò)請求,等等顷级。

這是一段極其常見的代碼:

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

在這里凫乖,cell屬于view,但是直接由model來配置弓颈,MVC的原則被打破了,這種寫法太常見了删掀,人們都不覺得這里有什么問題翔冀。如果我們嚴(yán)格遵循MVC的原則,在controller中配置cell披泪,不把Model傳給view, 那就要往已經(jīng)臃腫的controller里塞進(jìn)更多代碼纤子。

如果不寫單元測試,這個問題可能還不那么明顯。由于controller與view的緊密耦合控硼,測試變的很困難泽论,因?yàn)椴坏貌灰獎?chuàng)造性的模擬view及其生命周期。

來看一個簡單的例子:

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: Selector(("didTapButton:")), for: .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: "John", lastName: "Smith")
let view = GreetingViewController()
view.person = model

這看上去不太好測試卡乾。我們可以把greeting的生成移到Model里翼悴,分開測試,而我們想要測試GreetingViewController中那些展示邏輯的話幔妨,就必須直接調(diào)用UIView相關(guān)的方法(viewDidLoad, didTapButton)鹦赎,這樣的話,就需要加載所有的view误堡,這對單元測試來說可不是好事古话。

View和controller之間的交互,恐怕就是無法用單元測試來測試锁施。

到這里陪踩,看上去Cocoa MVC是一個糟糕的設(shè)計(jì)模式。讓我們用前文提到的特性列表來評估一下:

  1. 分布:View和Model確實(shí)是分開的悉抵,但是view和controller是緊密耦合的肩狂。
  2. 可測試性:由于糟糕的分布,你可能只會測試Model基跑。
  3. 易用:在所有模式中代碼量是最少的婚温。另外,對所有人來說都很熟悉媳否,所以即使是生手也能維護(hù)栅螟。

如果你不打算花大量時間在架構(gòu)上,Cocoa MVC將是你的選擇篱竭,或者你覺得用一個較高維護(hù)成本的架構(gòu)開發(fā)一個小項(xiàng)目是殺雞用牛刀力图。

對于開發(fā)速度來說,Cocoa MVC是最好的設(shè)計(jì)模式。

MVVM

MVVM是MV(X)系列中最新的一種,希望之前MV(X)中存在的一些問題屹培,他都考慮進(jìn)去了比肄。

理論上Model-View-ViewModel看上去很不錯。ViewModel我們已經(jīng)很熟悉了泪喊,還包括中間人,由View Model表示。

MVVM
  • MVVM把view controller視作View募舟。在這里,View Controller的子類實(shí)際上屬于View闻察,而不是中間人拱礁。
  • View和Model之間沒有緊密耦合琢锋。
  • 中間人在這里表達(dá)為View Model

另外呢灶,在viewview model之間存在binding吴超。

那么在iOS現(xiàn)實(shí)中,view model是什么呢鸯乃?他基本上是view及其狀態(tài)的代表鲸阻,并且是與UIKit無關(guān)的。View Model調(diào)用Model的變化飒责,同時根據(jù)Model的更新赘娄,更新自己。由于view和view model之間存在binding宏蛉,view也會相應(yīng)更新遣臼。

Bindings

Mac OS直接就支持Binding,但是iOS并不支持拾并。當(dāng)然iOS支持KVO和notification揍堰,但是沒有binding方便。

假設(shè)不想自己實(shí)現(xiàn)嗅义,我們有兩個選項(xiàng):

事實(shí)上幽纷,現(xiàn)在當(dāng)談到MVVM時,總是和ReactiveCocoa等聯(lián)系在一起的博敬,反之亦然友浸。雖然可以通過簡單的binding搭建MVVM,用ReactiveCocoa等可以充分發(fā)揮MVVM偏窝。

在我們簡單的例子里收恢,F(xiàn)RP框架甚至KVO都不需要。我們將顯式的通過showGreeting方法讓view model更新祭往,用一個簡單的屬性作為greetingDidChange回調(diào)函數(shù)伦意。

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: Selector(("showGreeting")), for: .touchUpInside)
    }
    // layout code goes here
}
// Assembling of MVVM
let model = Person(firstName: "John", lastName: "Smith")
let viewModel = GreetingViewModel(person: model)
let view = GreetingViewController()
view.viewModel = viewModel

我們再來評估一下特性:

  • 分布:MVVM的view比MVP的view責(zé)任更多,他通過設(shè)置binding硼补,根據(jù)view model更新狀態(tài)默赂。

  • 可測試性:view model對view一無所知,所以測試比較容易括勺。View或許也能測缆八,但是因?yàn)橐蕾囉赨IKit,可能就想跳過了

  • 易用性:如果采用binding疾捍,MVVM的代碼量會大為減少

MVVM是很有吸引力的奈辰,他不僅保持了之前方案的優(yōu)點(diǎn),而且因?yàn)閎inding在view中的采用乱豆,view更新不需要額外的代碼奖恰。與此同時,測試也比較便利宛裕。

VIPER

最后是VIPER瑟啃,不屬于MV(X)家族。

現(xiàn)在揩尸,你一定同意其職責(zé)的細(xì)分很優(yōu)秀蛹屿。VIPER在職責(zé)的細(xì)分上更進(jìn)了一步,有了5層岩榆。

VIPER
  • Interactor: 包含了與數(shù)據(jù)(Entity)或者網(wǎng)絡(luò)相關(guān)的業(yè)務(wù)邏輯错负,例如創(chuàng)建數(shù)據(jù)的新實(shí)例,從服務(wù)器獲取數(shù)據(jù)勇边。對于那些任務(wù)犹撒,一般會使用一些Service或者Manager,這些一般不被認(rèn)為是VIPER模塊的組成部分粒褒,而認(rèn)為是外部依賴识颊。

  • Presenter: 包含與UI相關(guān)(同時與UIKit無關(guān))的業(yè)務(wù)邏輯,會調(diào)用Interactor的方法奕坟。

  • Entities: 普通的數(shù)據(jù)對象祥款,但不是數(shù)據(jù)存取層,因?yàn)槟菍儆贗nteractor負(fù)責(zé)的执赡。

  • Router: 負(fù)責(zé)VIPER各模塊之間的轉(zhuǎn)移镰踏。

VIPER模塊既可以是單一屏幕,也可以是應(yīng)用的整個user story沙合,比方說用戶認(rèn)證奠伪,可以是一個屏幕也可以是多個相關(guān)的屏幕來實(shí)現(xiàn)。一塊積木有多大首懈,是由你決定的绊率。

通過與MV(X)類的比較,可以發(fā)現(xiàn)在責(zé)任的分布上是有些不同的:

  • Model(數(shù)據(jù)交互)邏輯移到了Interactor究履,而Entity只是啞的數(shù)據(jù)結(jié)構(gòu)滤否。

  • Controller/ViewModel中,只有UI表達(dá)的職責(zé)交給了Presenter最仑,而不包括改變數(shù)據(jù)的能力藐俺。

  • VIPER是第一個明確談到導(dǎo)航職責(zé)的模式炊甲,由Router來解決。

對于iOS應(yīng)用來說欲芹,實(shí)現(xiàn)合適的路徑導(dǎo)航是一個具有挑戰(zhàn)性的任務(wù)卿啡。MV(X)直接忽略了這個問題。

這個例子里面沒有包括導(dǎo)向和模塊之間交互的內(nèi)容菱父。

import UIKit

struct Person { // Entity (usually more complex e.g. NSManagedObject)
    let firstName: String
    let lastName: String
}

struct GreetingData { // Transport data structure (not Entity)
    let greeting: String
    let subject: String
}

protocol GreetingProvider {
    func provideGreetingData()
}

protocol GreetingOutput: class {
    func receiveGreetingData(_ greetingData: GreetingData)
}

class GreetingInteractor : GreetingProvider {
    weak var output: GreetingOutput!

    func provideGreetingData() {
        let person = Person(firstName: "John", lastName: "Smith") // usually comes from data access layer
        let subject = person.firstName + " " + person.lastName
        let greeting = GreetingData(greeting: "Hello", subject: subject)
        self.output.receiveGreetingData(greeting)
    }
}

protocol GreetingViewEventHandler {
    func didTapShowGreetingButton()
}

protocol GreetingView: class {
    func setGreeting(_ greeting: String)
}

class GreetingPresenter : GreetingOutput, GreetingViewEventHandler {
    weak var view: GreetingView!
    var greetingProvider: GreetingProvider!

    func didTapShowGreetingButton() {
        self.greetingProvider.provideGreetingData()
    }

    func receiveGreetingData(_ greetingData: GreetingData) {
        let greeting = greetingData.greeting + " " + greetingData.subject
        self.view.setGreeting(greeting)
    }
}

class GreetingViewController : UIViewController, GreetingView {
    var eventHandler: GreetingViewEventHandler!
    let showGreetingButton = UIButton()
    let greetingLabel = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.showGreetingButton.addTarget(self, action: Selector(("didTapButton:")), for: .touchUpInside)
    }

    func didTapButton(button: UIButton) {
        self.eventHandler.didTapShowGreetingButton()
    }

    func setGreeting(_ greeting: String) {
        self.greetingLabel.text = greeting
    }

    // layout code goes here
}
// Assembling of VIPER module, without Router
let view = GreetingViewController()
let presenter = GreetingPresenter()
let interactor = GreetingInteractor()
view.eventHandler = presenter
presenter.view = view
presenter.greetingProvider = interactor
interactor.output = presenter

我們再來分析下特性:

  • 分布:毫無疑問颈娜,VIPER是在職責(zé)分布上是做的最好的。
  • 可測試性:更好的分布帶來了更好的可測試性浙宜。
  • 易用性:最后官辽,和你猜的一樣,上述兩者需要付出的代價(jià)是可維護(hù)性粟瞬。你不得不為類寫大量的接口同仆,每個只負(fù)責(zé)很小的部分。

總結(jié)

我們介紹了幾種設(shè)計(jì)模式亩钟,希望能對你有所幫助乓梨。可能你也意識到了清酥,沒有銀彈扶镀,所以需要你在遇到實(shí)際問題時,權(quán)衡利弊焰轻,選擇架構(gòu)臭觉。

因此,可以在一些App中采用混合架構(gòu)辱志。舉個例子蝠筑,用MVC起步,然后發(fā)現(xiàn)采用MVC揩懒,有一個屏幕難以有效管理什乙,可以只針對這個屏幕切換到MVVM。沒有必要重構(gòu)其他MVC工作的好好的屏幕已球,這兩個架構(gòu)是很容易兼容的臣镣。

Everything should be made as simple as possible, but no simpler. ? Albert Einstein

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市智亮,隨后出現(xiàn)的幾起案子忆某,更是在濱河造成了極大的恐慌,老刑警劉巖阔蛉,帶你破解...
    沈念sama閱讀 222,378評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件弃舒,死亡現(xiàn)場離奇詭異,居然都是意外死亡状原,警方通過查閱死者的電腦和手機(jī)聋呢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評論 3 399
  • 文/潘曉璐 我一進(jìn)店門苗踪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人坝冕,你說我怎么就攤上這事徒探。” “怎么了喂窟?”我有些...
    開封第一講書人閱讀 168,983評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長央串。 經(jīng)常有香客問我磨澡,道長,這世上最難降的妖魔是什么质和? 我笑而不...
    開封第一講書人閱讀 59,938評論 1 299
  • 正文 為了忘掉前任稳摄,我火速辦了婚禮,結(jié)果婚禮上饲宿,老公的妹妹穿的比我還像新娘厦酬。我一直安慰自己,他們只是感情好瘫想,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,955評論 6 398
  • 文/花漫 我一把揭開白布仗阅。 她就那樣靜靜地躺著,像睡著了一般国夜。 火紅的嫁衣襯著肌膚如雪减噪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,549評論 1 312
  • 那天车吹,我揣著相機(jī)與錄音筹裕,去河邊找鬼。 笑死窄驹,一個胖子當(dāng)著我的面吹牛朝卒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播乐埠,決...
    沈念sama閱讀 41,063評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼抗斤,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了饮戳?” 一聲冷哼從身側(cè)響起豪治,我...
    開封第一講書人閱讀 39,991評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎扯罐,沒想到半個月后负拟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,522評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡歹河,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,604評論 3 342
  • 正文 我和宋清朗相戀三年掩浙,在試婚紗的時候發(fā)現(xiàn)自己被綠了花吟。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,742評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡厨姚,死狀恐怖衅澈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情谬墙,我是刑警寧澤今布,帶...
    沈念sama閱讀 36,413評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站拭抬,受9級特大地震影響部默,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜造虎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,094評論 3 335
  • 文/蒙蒙 一傅蹂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧算凿,春花似錦份蝴、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至戒努,卻和暖如春请敦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背储玫。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評論 1 274
  • 我被黑心中介騙來泰國打工侍筛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人撒穷。 一個月前我還...
    沈念sama閱讀 49,159評論 3 378
  • 正文 我出身青樓匣椰,卻偏偏與公主長得像,于是被迫代替她去往敵國和親端礼。 傳聞我的和親對象是個殘疾皇子禽笑,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,747評論 2 361

推薦閱讀更多精彩內(nèi)容