iOS MVP模式重構實踐

最近完成了我司iOS項目的重構,把總體的代碼架構都梳理了一遍,主要按照MVP的架構模式,並綜合考慮了重構的難度和效果。在這個過程當中也積累了一些代碼重構方面的經驗,在這裏總結一下。數據庫

項目簡介和MVP模式重構

項目簡介

首先簡單介紹一下項目狀況。咱們原有項目的架構是比較標準的MVC模式,也是蘋果官方推薦的架構模式。Model層用來表示實體類,View層負責界面展現和傳遞UI事件,Controller層負責大部分的業務邏輯。除此以外,對一部分公共的可複用的邏輯,咱們抽象出Service層,提供給Controller使用,另外網絡層也獨立出來。下圖比較清楚地展現了總體架構 編程

總體架構

MVC模式的問題

MVC架構做爲蘋果官方推薦的架構模式,把數據Model和展示View經過Controller層隔離開,在項目規模較小的時候是一個不錯的選擇。隨着項目複雜性的提升,咱們也漸漸感受到MVC模式的弊端,主要體如今下面幾個方面設計模式

  • Controller層職責過多,Model和View層太簡單

Controller處理業務邏輯,處理UI更新,處理UI事件,同步Model層,咱們幾乎全部的代碼都寫在了Controller層。設計模式裏有單一模式原則,你看這裏的Controller層已經至少有四種職責了。bash

  • 業務邏輯和UI混雜在一塊兒,難以編寫單元測試

這一點一方面是由於Cocoa框架裏的Controller層,就是咱們最熟悉的UIViewController和View是自然耦合的,不少view的生命週期方法如viewWillAppear都存在於VC,另外一方面咱們不少時候也習慣於把UI操做甚至初始化操做放在VC裏,致使UI和業務邏輯混雜在一塊兒。當你想對業務邏輯編寫單元測試的時候,看着業務邏輯代碼裏混雜的UI操做,就知道什麼叫舉步維艱——數據能夠Mock,UI是不可能被Mock的。網絡

  • 業務邏輯代碼大量存在於Controller層,維護困難

當一個界面功能比較複雜的時候,咱們全部的邏輯代碼都會堆積在Controller中,好比咱們原有的WebViewController的代碼就多達5000行,在這種狀況下維護代碼簡直是如履薄冰。架構

MVP模式的重構

對於Controller層過於臃腫的問題,MVP模式則能較好地解決這個問題——既然UIViewControllerUIView是耦合的,索性把這二者都歸爲View層,業務邏輯則獨立存在於Presenter層,Model層保持不變。下圖比較清除得展現了MVP模式的結構 app

MVP模式簡介
咱們來看一下MVP模式可否解決MVC模式存在的問題

  • Controller層職責過多,Model和View層太簡單

在MVP模式下,Controller層和View層已經合併爲View層,專門負責處理UI更新和事件傳遞,Model層仍是做爲實體類。本來寫在ViewController層的業務邏輯已經遷移到Presenter中。MVP模式較好地解決了Controller層職責過多的問題。框架

  • 業務邏輯和UI混雜在一塊兒,難以編寫單元測試

Presenter層主要處理業務邏輯,ViewController層實現Presenter提供的接口,Presenter經過接口去更新View,這樣就實現了業務邏輯和UI解耦。若是咱們要編寫單元測試的話,只須要Mock一個對象實現Presenter提供的接口就行了。MVP模式較好地解決了UI和邏輯的解耦。單元測試

  • 業務邏輯代碼大量存在於Controller層,維護困難

經過把業務邏輯遷移到Presenter層,Controller層的困境彷佛獲得瞭解決,可是若是某個需求邏輯較爲複雜,單純的把業務邏輯遷移解決不了根本的問題,Presenter層也會存在大量業務邏輯代碼,維護困難。這個問題,咱們下面會討論如何解決。學習

MVC模式改進——Router模式

這裏主要是考慮界面間跳轉的代碼如何重構,這一點我在以前的文章裏已經有提到了,這裏給個連接iOS重構之面向協議編程實踐,另外附圖一張

Router模式

實例分析

前面咱們提到,MVP模式雖然能解決許多MVC模式下存在的問題,但對於比較複雜的需求,仍是會存在邏輯過於複雜,Presenter層也出現難以維護的問題。下面咱們就經過一個實際的例子,來看看面對複雜的業務邏輯,咱們應該如何去設計和實現。

不少複雜的需求,在最初都是從一個簡單的場景,一步步往上增長功能。在這個過程當中,若是不持續的進行優化和重構,到最後就成了所謂的"只有上帝能看懂的代碼"。說了這麼多,進入正題,來看這個需求。

V1.0 單文件上傳

實現一個簡單的單文件上傳,文件的索引存儲在數據庫中,文件存儲在App的沙箱裏面。這個應該對於有經驗的客戶端開發者來講是小菜一碟,比較簡單也容易實現。咱們能夠把這個需求大體拆分紅如下幾個子需求

  1. 初始化上傳View
  2. 更新上傳View
  3. 點擊上傳按鈕事件
  4. 數據庫中獲取上傳模型
  5. 發起HTTP請求上傳文件
  6. 檢查網絡狀態

以上幾項若是使用傳統的MVC模式,實現起來以下圖所示

MVC
咱們能夠看到上述需求基本都直接在 UploadViewController中實現,目前需求仍是比較簡單的情形下面,仍是勉強可以接受,也不須要更多的思考。若是使用MVP的模式進行優化,以下圖所示

MVP.png

如今UploadPresenter負責處理上傳邏輯了,而UploadViewController專一於UI更新和事件傳遞,總體的結構更加清晰,之後維護代碼也會比較方便。

V2.0 多文件上傳

需求來了!須要在原來的基礎上支持多文件上傳,意味着咱們多了一個子需求

  1. 維護上傳模型隊列

很顯然,咱們須要在UploadPresenter中增長一個維護上傳隊列的功能,最初我也確實是這樣實現的,可是因爲文件上傳須要監聽的事件比較多,回調也比較頻繁,直接在Presenter中繼續寫這樣的邏輯代碼,已經成倍增長了代碼的複雜性。

因此通過一番思考,我考慮把文件上傳這部分的邏輯單獨提取出一層FileUploader,而UploadPresenter只負責維護FileUploader的隊列以及檢查網絡狀態。具體的實現以下所示。

MVP2.png
咱們能夠看到,分層以後的結構又更加清晰了,每一層的職責都比較單一,目前看起來一切OK!

V3.0 多來源上傳

原來咱們的上傳文件的來源是存在於App沙箱裏的,咱們經過數據庫查詢能夠找到這個文件的索引和路徑,進而獲取到這個文件進行上傳。如今萬惡的需求又來了,須要支持上傳系統相冊中獲取的圖片/視頻。

  1. 支持系統相冊和App沙箱中獲取文件

到這裏可能有些讀者已經有點頭大了,若是沒有仔細思考,極可能從這裏就走向了代碼質量崩潰的道路。

這個時候,咱們就要思考,他們是多來源,可是對於FileUploader來講,它其實不關心模型的來源,它只須要獲取到模型的二進制流。因而,咱們能夠抽象出一個BaseModel,提供一個stream只讀屬性,兩種來源分別繼承BaseModel,各自重載stream只讀屬性,實現本身的構造文件stream的方法。對於FileUploader來講,它只持有BaseModel便可,這就是繼承和多態的一個典型的使用場景

若是後續還有更多來源的文件,好比網絡文件(先下載再上傳?),也只須要繼續繼承BaseModel,重載stream便可,對於FileUploader和它的全部上層來講,一切都是透明的,無需進行修改。通過這樣的設計,咱們的代碼的可維護性和可擴展性又好了。下面是架構圖。

MVP3.png

V4.0 多方式上傳

在HTTP文件上傳中,咱們能夠直接上傳文件的二進制流,這種就須要服務端作特定的支持。但更爲經常使用和支持普遍的作法是使用HTTP表單文件傳輸,即組裝HTTP請求的body時採用multipart/form-data的標準組裝,傳輸數據。因而,咱們又多了一個需求:

  1. 支持表單傳輸和流傳輸

思路和剛纔的多來源上傳差很少,咱們把上面的兩種來源的模型,即FSBaseMABaseM抽象爲父類,父類含有各自的文件二進制數據的抽象,子類分別實現二進制直接組裝流,和按multipart/form-data格式組裝流,實現以下圖。

MVP4.png

V5.0 支持FTP/Socket上傳

剛纔咱們的文件上傳,底層的協議是基於Http,此時咱們須要支持FTP/Socket協議的傳輸,應該怎麼辦?

  1. 支持HTTP/FTP/Socket

通過上面的思考,相信你必定知道該怎麼作了。這裏留個思考,答案請戳這裏MVP_V5架構

對比

最後,咱們把目前的需求全都整理一下

  1. 初始化上傳View
  2. 更新上傳View
  3. 點擊上傳按鈕事件
  4. 數據庫中獲取上傳模型
  5. 發起HTTP請求上傳文件
  6. 檢查網絡狀態
  7. 維護上傳模型隊列
  8. 支持系統相冊和App沙箱中獲取文件
  9. 支持表單傳輸和流傳輸
  10. 支持HTTP/FTP/Socket

咱們看看,若是分別採用MVC、MVP_V一、MVP_V二、MVP_V三、MVP_V四、MVP_V5,來實現目前的十個需求,咱們的代碼大體會分佈在哪幾層。

優化後的架構模式之間的比較

孰優孰劣一目瞭然。若是採用最原始的MVC模式的話,保守估計ViewController代碼量至少3K行以上。

總結

  • 運用MVP的設計模式,邏輯和UI操做解耦
  • 分層模式,上層擁有下層,下層經過接口與上層通訊,達到解耦。
  • 利用繼承和多態,屏蔽底層實現的細節,達到職責分離和高擴展性

代碼優化和重構的技巧

在此次的項目重構中,我也總結了一些重構方面的技巧和貼士,但願能幫助到想開始進行代碼重構的同窗

事不過三

  • 大段重複的代碼出現了三次或以上 ——提取成一個公共的方法 這一點是最多見也最容易作到的,只要在平時的編碼過程當中養成這種習慣,對於出現過三次以上重複代碼段,提取成一個公共方法。

  • 一個類的職責有三種或以上 ——經過合理分層的方式,減小職責 這一點在上面的例子中已經闡述地比較清楚了,經過職責的分層,上層持有下層,下層經過接口與上層通信。其實這也是MVP模式的本質。

  • 同類的if/else出現了三次或以上 ——考慮使用抽象類和多態代替if/else 若是相同的if/else判斷在你的代碼中出現了不少次的話,則應該考慮設計一個抽象類去替代這些判斷。這裏可能有點難以理解,舉個例子就好懂不少 好比,如今咱們有一個水果類,有三種水果,水果有顏色、價錢和品種

class Fruit {
    
    var name:String = ""
    
    func getColor() -> UIColor? {
        if name == "apple" {
            return UIColor.red
        } else if name == "banana" {
            return UIColor.yellow
        } else if name == "orange" {
            return UIColor.orange
        }
        return nil
    }

    func getPrice() -> Float? {
        if name == "apple" {
            return 10
        } else if name == "banana" {
            return 20
        } else if name == "orange" {
            return 30
        }
        return nil
    }
    
    func getType() -> String? {
        if name == "apple" {
            return "紅富士"
        } else if name == "banana" {
            return "芭蕉"
        } else if name == "orange" {
            return "皇帝"
        }
        return nil
    }
}
複製代碼

這裏的對名稱name的相同的if/else判斷出現了三次,若是此時咱們多了一種水果梨,咱們得修改上述全部的if/else判斷,這樣就會很是難維護。

這種場景咱們能夠考慮抽象出一個Fruit的抽象類/接口/協議,經過實現水果類/接口/協議的方式,此時若是多了一種水果,讓這種水果繼續實現Fruit協議就行,這樣咱們就經過新增的方式替代修改,提升了代碼的可維護性。

protocol Fruit {
    func getPrice() -> Float?
    func getType() -> String?
    func getColor() -> UIColor?
    var name:String { get }
}

class Apple:Fruit {
    var name:String = "apple"
    func getColor() -> UIColor? {
        return UIColor.red
    }
    
    func getPrice() -> Float? {
        return 10
    }
    
    func getType() -> String? {
        return "紅富士"
    }
}

class Banana:Fruit {
    var name:String = "banana"
    func getColor() -> UIColor? {
        return UIColor.yellow
    }
    
    func getPrice() -> Float? {
        return 20
    }
    
    func getType() -> String? {
        return "芭蕉"
    }
}

class Orange:Fruit {
    var name:String = "orange"
    func getColor() -> UIColor? {
        return UIColor.orange
    }
    
    func getPrice() -> Float? {
        return 30
    }
    
    func getType() -> String? {
        return "皇帝柑"
    }
}
複製代碼

合理分層

  • 縱向分層——層級之間有關聯 上層持有下層,下層經過接口與上層通訊。這裏爲何不讓下層也持有上層呢?主要仍是爲了可以解耦,下層設計的目的是爲上層服務的,它不該該依賴上層。這種設計模式在計算機科學中是很常見的,好比計算機網絡中的網絡分層設計。
  • 橫向分層——層級之間無關聯 適用於功能相對獨立的模塊,簡單劃分便可。咱們的iOS項目的首頁就是由好幾個部分組成,這個部分之間無太多的關聯,咱們簡單劃分紅幾個模塊就行。若是出現了少數須要通信的場景,使用Notification便可。

避免過分設計

  • 越簡單的越是有效的 複雜的架構設計每每在客戶端高速迭代開發中意義不大(相比服務端)
  • 沒有銀彈! 軟件開發是工程化的,沒有完美的架構模式,不少時候須要具體問題具體分析,靈活運用設計模式,獲得局部的最優解。好比前面提到的MVP模式,若是生搬硬套,一樣沒法解決Presenter層複雜的問題。
  • 如何判斷過分設計? 膠水代碼過多 大量文件的行數小於100 想了一天,沒寫出代碼,也沒寫出架構方案

重構的時機和對象

  • 時機 單文件代碼行數開始超過500行的時候 Code Review是重構的好幫手
  • 對象 需求常常變化或增長的功能,必定要注意設計,避免走向質量不可控 穩定且不變的功能,不重構

總結

最後我想談談設計模式。其實重構的過程其實也就是靈活運用設計模式對代碼進行優化和改進。不少人設計模式也看了不少,學習了不少,但真正在工做中能合理使用的卻不多。因此關鍵還在靈活運用四個字上,能作到這一點,你的水平就會上一個臺階。

因此在平時的工做中,咱們要有對代碼的Taste,知道什麼樣的是好代碼,什麼樣的是髒代碼,儘早發現可優化可改進的地方,持續產出高質量代碼,而不是實現功能就萬事大吉,不然早晚要爲你之前偷的懶買單。 以上就是我在我司項目重構過程當中的的一些總結和分享,水平有限,但願對你們有所幫助。

相關文章
相關標籤/搜索