當前位置:
首頁 > 最新 > 蜂鳥商家版 iOS 組件化/模塊化實踐總結

蜂鳥商家版 iOS 組件化/模塊化實踐總結

零. 前言

「蜂鳥配送商家版」是一款針對商家打造的專業配送軟體,有了這款應用,您可以使用蜂鳥商家版呼叫所有平台訂單及電話訂單配送,餐飲、鮮花、蛋糕、生鮮、商超均可配送。超低運費,清晰合理。海量補貼,充值返現。

以上這段對「蜂鳥商家版」的描述摘自蜂鳥配送官網,大概可以理解為蜂鳥商家版是一個給廣大商家用來發單呼叫配送員的 App。許多同學可能只聽說過「餓了么」外賣應用,但是對支撐起外賣配送的後勤業務「蜂鳥配送」卻知之甚少,實際上每天海量的外賣訂單都是由蜂鳥配送系統進行處理和配送最終送到消費者手中的。外賣 O2O 是由外賣平台、商戶、配送系統這三方合作共同完成的,缺一不可。O2O 最核心的價值就是人與服務的連接,而這種連接最終都是通過配送才得以實現的。

自 2016 年底開始我參與蜂鳥商家版的維護工作,除了日常的開發迭代以外,期間還參與推進了項目 Swift 化、項目組件化 / 模塊化、非業務組件開源化等技術改造工作,今天這篇文章就給大家分享一下蜂鳥商家版 iOS 的組件化 / 模塊化實踐過程和自己的心得體會。

一. 背景分析

蜂鳥商家版 iOS 端代碼使用 Git 進行管理,代碼託管在內網的 GitLab 上。項目的依賴管理工具是大家比較熟悉的 CocoaPods,除了 RN 模塊為了和 Android 組公用採用 Submodule 進行管理外,其他所有的子模塊都採用 Pods 庫的方式引入。

1. 存在的問題

在「蜂鳥商家版 iOS 組件化 / 模塊化」工作開展之前,項目主要存在如下這些問題:

項目臃腫不堪

在組件化 / 模塊化之前,蜂鳥商家版 App 的所有代碼 / 資源文件等都是在同一個主工程里的,只有 RN 倉庫或組內公用私有庫等極少部分代碼遊離於主工程之外,所以在開發時,每一次都要編譯整個項目的所有代碼,十分低效。這個問題在獨立開發時還不是十分明顯,畢竟雖然項目大但是代碼只有一個人在提交,所以項目代碼量增加也不是那麼誇張而且對項目發生的變化比較熟悉。但是當多人協作開發時,這個缺陷就暴露了出來,大家在各自開發不同的業務時,不僅要時刻和他人同步項目變化、讀懂他人代碼,還要每次編譯完整個項目才能對自己所做的一點修改進行調試,效率低下。

團隊規模變化

我開始參與蜂鳥商家版 iOS 端的維護時,之前只有一個前輩在維護,也就是一個人獨立維護一個 App。然後過了沒多久,他離職去了另一家公司,所以又變成了一個人獨立維護這個 App。這時候因為是獨立開發,所以也不存在什麼太大的問題。但隨著團隊擴大,後面陸續來了幾位同事共同負責這個項目的維護工作,大家都在同一個工程上進行業務開發,經常遇到如代碼衝突、開發效率低下、職責劃分不清、代碼管理混亂等問題。

業務發展壓力

由於公司處在高速發展的階段,業務增長很快,最直觀的表現就是市場 & 客服部門不斷接到大量一線使用者的使用反饋或訴求,最後就變成了產品展示給我們開發人員的一份接一份的 PRD。緊湊的業務開發需求和各種靈活的功能迫使我們想盡一切能夠使用的辦法來提高開發效率,提高提測質量。

代碼管理混亂

當我開始參與這個項目的維護時,這個項目就已經是一個 Swift 和 OC 混編的項目了,然後還有 RN 和 H5 代碼,可以說是十分複雜了。雖然這不是我廠唯一一個 Swift 和 OC 的混編項目,但絕對是當時 Swift 化最高的一個項目,約 25% 的代碼為 Swift。眾所周知,Swift 和 OC 的互相調用遠不如 Java 和 Kotlin 的互相調用那麼順滑(反正你現在知道了),並且處處藏著危機,暗坑無數,所以迫切需要找一個方式,將 Swift 和 OC 代碼進行整理、轉換或者分隔。畢竟,這個文件是 OC 下一個文件就是 Swift 這種頻繁的思維轉換在業務開發這種本就十分緊張的場景下,會使人十分疲憊,不利於開發工作的順利進行。

2. 怎樣去解決

為了解決以上這些問題,我們曾經進行過如下一些探索:

移除無用的第三方庫和資源文件,減少打包時間:效果不明顯;

整理並推動內部 Gitflow 工作流,提高協作效率:有一些效果,但由於項目過大,日常協作仍然吃力;

研究 Swift 編譯時間優化方法,提高編譯效率:發現增加編譯時間的都是 Swift 的一些常用語法糖,如果不用的話,嚴重降低開發效率,遂放棄;

在不拆分主工程的情況下,推動項目整個 Swift 化:由於之前維護項目的前輩離職,導致目前的項目開發人員都對原代碼不是十分熟悉,不敢妄加改動,加之業務迭代頻繁,開發和測試資源都十分緊張,該工作工作推進十分緩慢。

可以發現上述嘗試的結果都不是十分理想,在與 iOS 組內大佬們進行一些溝通,聽取大佬們的意見後,決定對原項目進行「組件化 / 模塊化拆分」工作,它能帶來如下這些好處:

加快編譯速度,不用再編譯組件 / 模塊外沒有被依賴到的代碼;

便於將每個模塊指定給不同負責人進行管理;

降低合併難度,減小衝突和出錯概率,提高業務開發效率;

將 Swift 和 OC 代碼進行分離,便於進一步 Swift 化工作的推進;

可為模塊編寫單元測試,提高工作效率,同時方便測試人員進行有針對性的測試。

二. 目標設定

功能組件獨立:保證所有的底層功能組件從主工程抽出,獨立與主工程之外,便於復用、業務模塊的調用;

業務模塊劃分與拆解:將業務按對應用途進行劃分和拆解,想辦法切斷各業務之間的強依賴;

所有組件 / 模塊獨立編譯:所有功能組件和業務模塊能夠獨立於主工程進行編譯,有各自的 Demo 工程;

CocoaPods 發布:在內網 GitLab 進行發布,並且之後對每個模塊用 GitFlow 工作流進行管理和後續發布工作。

三. 計劃制定

說到組件化 / 模塊化,那麼什麼是組件化 / 模塊化呢?組件化和模塊化的區別又在哪裡呢?

組件,就是我們對功能的封裝,一個功能就是一個組件,資料庫、網路、文件操作、社會化分享等等這些功能都是組件。我們之所以要搞出組件的概念,是為了能夠讓我們的上層業務模塊能夠隨時依賴和調用這些基礎功能。組件基本上可以分為基礎功能組件、通用 UI 組件、基礎業務組件等這幾類。所以為了滿足上述要求,組件必須具有較高的獨立性、擴展性以及復用性。

模塊,就是對一系列有內聚性的業務進行整理,將其與其它業務進行切割、拆分,從主工程或原所在位置抽離為一個相對獨立的部分。僅僅針對業務而言,比如說我們可以把訂單業務獨立為為一個模塊,可以把個人中心獨立為一個模塊,把用戶登錄獨立為一個模塊等,在 App 中的體現就是一個個獨立的 Git 倉庫。模塊化的一個好處是用到時可以搭積木,比如可以多個工程間復用同一個或幾個業務模塊,比如騰訊的 QQ 和 TIM,除了 UI 界面外 TIM 顯然復用了大量現有的原 QQ 工程的業務模塊代碼,當然,我們這裡暫時並沒有這個需求。

經過小組會議討論,我們的想法是將共用組件獨立出來,然後直接按業務對現有主工程進行拆分同時兼顧 Swift 與 OC 分離,大致劃分如下表所示:

1. 組件

2. 模塊

3. 關係

按照上面的思路,理想化的模塊 / 組件依賴關係圖大概是這個樣子的:

因為蜂鳥商家版的團隊開發人員之前均沒有過任何項目的拆分經驗,大家也都是摸著石頭過河,走一步看一步。所以雖然以上的拆分思路總體是對的,先拆組件後拆業務,但由於各種各樣的原因,一些問題就在接下來的工作實施過程中暴露了出來。

四. 工作實施

我們小組主要還是以業務開發為主,所以組件化 / 模塊化工作都是大家抽空閑時間來完成,並沒有進行硬性的排期和設置 Deadline。按照之前制定的計劃,我們進行了以下這些工作:

1. 功能組件獨立

1.1 LPDBOCFoundationGarbage

LPDBOCFoundationGarbage 是我們項目最先抽出的部分,這個庫將和 LPDBPublicModule 一起,作為整個工程的最底層,再往下就是。這個庫的定位和它的名字一樣,就是一個垃圾桶,啥都往裡放。其中大致包含以下一些東西:

自定義的 View 和控制項,例如:小紅點控制項、刷新控制項、載入控制項、Tips 視圖等;

自定義的 Controller,例如:基礎控制器 BaseViewController、WebView 基礎控制器 BaseWebViewController、自定義的彈框 AlertController等;

和業務相關的對基本類型或系統控制項的擴展:對 NSObject、UIButton、UIImageView、UILabel 等添加的擴展代碼 category;

甚至版本控制模塊 LPDBVersionManager 也放在了這裡。

因為我們在進行拆分任務的同時,還在同時維持著項目的開發工作,所以我們暫時沒有精力做細緻的拆分工作,只能先把這些零散的部分先放在一起進行管理。

1.2 LPDBPublicModule

LPDBPublicModule 是基礎的 Swift 組件,這個庫主要包含:

一些公用的 Swift 擴展,例如:對 CGFloat、Date、NSString 等系統類型的 extension;

用於模塊間解耦的協議。

因為工程內的 Swift 代碼大多是我們新寫的,所以相對舊的 OC 代碼而言,整理地更好一些,所以這個倉庫乾淨很多

1.3 LPDBNetwork

LPDBNetwork 網路組件是我們項目完成 OC 和 Swift 基礎部分後最先抽出的部分,剛開始我們認為這部分僅僅是單純的業務網路請求操作和對 AFNetworking 的淺層封裝,不包含界面 UI 邏輯等。不過當我們拆解完成後,發現其中還包含了一堆奇怪的東西:

對 AFNetworking 的封裝和網路操作的一些定義,例如:LPDBHttpManager、LPDBRequestObject 和 LPDBModel 等;

UI 操作,例如:等待視圖 LPDBLoadingView 和 網路請求失敗的提示等。

這一部分的話,因為都是比較古老的代碼,所以當初的開發人員都已經不再繼續維護了,所以在只能是我們自己進行拆分的情況下,為了防止大的變更導致發生問題,所以沒有對這一塊進行更細緻的拆解工作。畢竟再爛代碼也比不能工作的代碼要好。

1.4 LPDBUIKit

Swift 的 UI 庫,我們將工程中的一些 Swift 視圖和控制項收集到了這個項目中,主要包含以下這些內容:

視圖,例如:LPDBEmptyDataView、SlideScrollView 等;

控制項,例如:SlideTabKit 等。

因為 Swift 代碼總量還不是很大,所以這個庫的東西目前也不是很多,以後會逐漸豐富起來。

2. 業務模塊拆分

完成了上面的組件庫的獨立工作後,業務模塊的拆解就相對輕鬆一些了,目前我們主要完成了三個業務模塊的拆分工作。

2.1 LPDBHistoryModule

LPDBHistoryModule 歷史訂單模塊,和歷史訂單頁面相關的信息都在該模塊中,主要包含以下內容:

UI,例如:歷史訂單界面、歷史訂單列表 Cell、載入視圖等;

數據模型,例如:歷史訂單模型;

歷史訂單列表相關的網路請求。

因為該模塊相對來說比較獨立,所以拆分過程也比較順利,主要依賴了 LPDBPublicModule、LPDBNetwork、LPDBOCFoundationGarbage 組件。

2.2 LPDBLoginModule

LPDBLoginModule 用戶登錄模塊是一個與用戶登錄、註冊以及用戶登陸信息有關的模塊,主要包含了以下信息:

UI,例如:用戶登陸界面、用戶註冊界面等;

數據模型,例如:用戶信息模型、用戶信息地址模型等;

登陸與註冊相關的網路請求。

該模塊相比較歷史訂單模塊複雜了一些,不過仍然比較順利,主要依賴了 LPDBPublicModule、LPDBOCFoundationGarbage、LPDBNetwork 組件。

2.3 LPDBUserCenterModule

LPDBUserCenterModule 用戶中心模塊是一個與用戶個人中心以及用戶信息修改有關的模塊,主要包含了以下信息:

UI,例如:用戶中心界面、用戶電話修改界面、用戶密碼修改界面等;

數據模型,例如:用戶詳細信息模型、用戶信息地址模型等;

用戶中心相關的網路請求,例如:修改電話號碼、請求驗證碼等。

該模塊主要依賴了 LPDBOCFoundationGarbage 組件和 LPDBLoginModule 模塊。

2.4 其它

剩下的其他一些模塊仍然處於計劃中的狀態,暫未進行拆分。到這一步的話,庫間依賴關係大致如下圖所示:

可以看到其中存在一些不太合理的依賴關係,如 LPDBUserCenterModule 依賴 LPDBLoginModule 模塊,也就是所謂的業務模塊橫向依賴問題,接下來,我們就要處理這一問題。

3. 解除耦合

由於之前開發過程中從未有過任何模塊化的考量,所以蜂鳥商家版的代碼非常雜糅,項目依賴關係十分複雜,主要可以分為以下三類耦合:

界面耦合:App 執行過程中,硬編碼的界面間的跳轉行為;

工程耦合:某些模塊在運行時需要依賴主工程的代碼才能運行或實現完整的功能;

依賴耦合:兩個業務模塊之間的有依賴。

3.1 模塊間組件共用

在拆分業務模塊的過程中,經常發生兩個業務模塊同時引用某一塊業務代碼的問題,這時我們就需要對這一塊代碼進行理解,首先區分它到底應不應該劃分到業務層來?

如果是的話,應該劃歸到哪一個模塊中去更合理一些;

如果不是的話,應該將這一部分代碼下沉到哪一個組件庫中去比較合適,或者獨立為一個組件。

在 LPDBUserCenterModule 的抽離過程中就遇到了這個問題,LPDBUserCenterModule

和 LPDBLoginModule 共同依賴了幾個和用戶信息有關的數據模型,導致需要發生模塊間橫向依賴,所以我們將共用的數據模型抽出,然後下沉到了 LPDBOCFoundationGarbage 中。

3.2 模塊間耦合

另一個經常遇到的問題就是跨模塊調用代碼的問題了,不僅是模塊與模塊間代碼的互相調用、模塊間頁面的跳轉,還有模塊反向調用主工程代碼等問題,這個問題的解決我們分了三步:

反射調用

因為工程的複雜性和以前代碼的不規範,導致我們在處理切割業務模塊時比較痛苦,所以我們在剛開始抽出模塊時採用了一種快速但不太安全的方式進行解耦,比如在 LPDBUserCenterModule 模塊中需要調用主工程的 getMiddlePageVC 方法時,我們用了如下臨時解決方案:

if ([[UIApplication sharedApplication].delegate respondsToSelector:@selector(getMiddlePageVC)]) {

UIViewController *info = [[UIApplication sharedApplication].delegate performSelector:@selector(getMiddlePageVC)];

...

}

然後在主工程的 中實現這個介面:

// .h

@interface AppDelegate : UIResponder

...

// LPDBUserCenterModule

- (UIViewController *)getMiddlePageVC;

...

@end

// .m

@implementation AppDelegate

...

- (UIViewController *)getMiddlePageVC {

...

return xxx;

}

...

@end

這一方案的優點就是靈活,利用 NSClassFromString、performSelector 等方式,能夠快速解決各種耦合問題,瞬間切割出模塊。但缺點也顯而易見,字元串硬編碼,維護成本大,去掉了編譯器檢查,容易翻車。

協議調用

所以自然而言地,當我們的某個業務模塊的拆分工作基本定型時,我們就開始將第一步中的反射調用方式替換為協議的方式進行調用,比如當 LPDBLoginModule 模塊需要調用主工程的 getCoordinate 方法時,示例如下:

id delegate = [[UIApplication sharedApplication] delegate];

if (![delegate conformsToProtocol:@protocol(AppDelegateProtocol)]) {

return;

}

CLLocationCoordinate2D coordinate = [delegate coordinate];

然後在主工程中實現該方法:

// .h

#import "AppDelegate.h"

@import LPDBLoginModule;

@interface AppDelegate (Protocol)

@end

// .m

@implementation AppDelegate (Protocol)

- (CLLocationCoordinate2D)getCoordinate {

return self.coordinate;

}

@end

但是,樣的改變並不能徹底解決所編寫的模塊間互相調用的代碼缺乏編譯器檢查的問題,而僅僅是對調用方做了判斷加上了容錯,並不能在編譯期就讓開發人員察覺到問題,一定要進行測試才可以,所以這種方式也不是十分理想。

Lotusoot 解耦工具

那麼為了徹底解決問題,我們開發和引入了組件通信和工具 Lotusoot,調用方式有下列幾種可供參考:

服務調用

let lotus = s(AccountLotus.self)

let accountModule: AccountLotus = LotusootCoordinator.lotusoot(lotus: lotus) as! AccountLotus

accountModule.login(username: "admin", password: "wow") { (error) in

print(error ?? "")

}

短鏈註冊

let error: NSError? = LotusootRouter.register(route: "newproj://account/login") { (lotusootURL) in

accountModule.showLoginVC(username: "admin", password: "wow")

}

短鏈調用

let param: Dictionary = ["username" : "admin",

"password" : "wow"]

// 無回調

LotusootRouter.open(route: "newproj://account/login", params: param)

// 有回調

LotusootRouter.open(route: "newproj://account/login", params: param).completion { (error) in

print(error ?? "open success")

}

// ??不推薦的用法,用 ?pram0=xxx 這樣的形式導致字元串散落在各處,不易管理。

// 但為了保證 Hybrid 項目中 H5 頁面的正常跳轉,提供了此種調用

LotusootRouter.open(url: "newproj://account/login?username=zhoulingyu").completion { (error) in

print(error ?? "open success")

}

具體可以參見iOS 靈活的 模塊化/組件化 工具與規範 Lotusoot 解說一文,在此不多做贅述。類似的工具還有BeeHive和LPDMvvmRouterKit等,大家可以自行進一步探索。

最終結構就變成了如圖所示的樣子:

五. 問題整理

1. 不合理的分層結構和庫間依賴

由於參與拆分工作的人員比較缺乏組件化經驗,所以導致某些庫的拆分不是十分合理,某些應該沉入底層的公用 Model 和常量等沒有在開始時就放到一個合理的位置。業務模塊之間也存在一些不合理的橫向依賴,沒有進行一個合理的業務邊界劃分。這些原因導致我們在進行拆分工作時經常需要回過頭來對已經拆出來的模塊和組件重新進行整理和處理,重複勞動量很大。

2. 拆分粒度不適中

某些庫比如 LPDBOCFoundationGarbage 比較龐大,而像 LPDBUIKit 這樣的庫中內容卻非常少,這一點的處理上存在問題。如果一個拆分完成的庫仍然比較臃腫的化,說明仍然存在細化拆分的必餘地。

3. 工作進度難以控制

由於沒有能提前制定好詳細的進度計劃表,加上業務工作的擠壓,導致我們花在組件化 / 模塊化工作上的時間比較零散。本意是希望大家能夠靈活安排工作,合理處置業務開發與技術改造工作之間的關係,但效果不是很理想,表現就是組件化 / 模塊化工作的進行沒有連續性,大家的積極性和工作效率也都不高。

六. 經驗總結

1. 工作開始前要進行技術調研

查看和學習一些同類成功的案例資料或者向業內大佬們請教能夠對計劃的制定帶來便利,能夠使我們避免很多錯誤的設計,少走一些彎路,降低返工率。

2. 制定詳細整體規劃

在準備作戰時,我常常發現定好的計劃沒有用處,但計劃的過程仍必不可少。—— 德懷特·艾森豪威爾

制定詳細的整體規劃能夠在設計階段就將一些不合理的地方暴露出來,從而拿出解決方案使問題提前得到解決,或者把不合理的內容刪減替換掉,例如分層不合理、庫間依賴這樣的問題,就會減少很多。拿出細緻的任務拆分計劃和工作量預估,也能更合理地將任務安排到開發人員手中,在提升工作效率的同時也能盡量避免和業務開發產生衝突。

3. 注意對代碼質量的控制

好的代碼和編碼習慣能夠大幅提升項目的可維護性,為之後的工作帶來便利。我們之前舊的 OC 代碼比較混亂,基本處於無法維護的狀態,拆分起來十分痛苦;而新寫的 Swift 代碼明顯質量要高很多(這真的不是我們自誇...),拆分起來就順利多了。

4. 重視信息的文檔化

每一個拆分出的模塊及時添加文檔,嫌麻煩的話至少要建立一份通用的 README 模板,每一個模塊或組件的建立者把模塊內容、拆分目的、設計思路等基本信息記錄一下,有什麼坑或者注意點也可以文檔化,是以後的長期項目維護成為可能。

七. 開源成果

我們在組件化 / 模塊化工作期間,產出的一些庫和工具放在了 GitHub 上進行開源,給大家一些借鑒的同時,也希望能夠收到大家的意見和建議,提高我們項目本身的質量:

Lotusoot - 靈活的 Swift 組件解耦和通信工具:https://github.com/Vegetarians/Lotusoot

Bamboots - 一個面向協議的網路庫:https://github.com/mmoaay/Bamboots

bigkeeper - 一個 iOS&Android 模塊化項目效率提升工具:https://github.com/BigKeeper/bigkeeper

SideNavigation - 一個支持側滑且可自定義的側邊欄:https://github.com/CNKCQ/SideNavigation

ViewPagers - 一個支持手勢的 Segmented Control:https://github.com/CNKCQ/ViewPagers

EFAutoScrollLabel - 一個帶跑馬燈效果的 UILabel:https://github.com/EyreFree/EFAutoScrollLabel

八. 後記

本文基本描述了蜂鳥商家版 App 到目前為止的組件化 / 模塊化實踐情況,希望本文能夠給您的移動項目演進提供一些借鑒。在此過程中我們產出的一些文章、開源庫和工具,也希望能給大家帶來一定的幫助或者啟發。歡迎大家提出各種反饋和建議或,幫助我們繼續改進和提高。

2017 年底,也就是差不多我參與蜂鳥商家版的維護工作滿一年的樣子,由於業務調整的原因這個 App 已經移交給別的團隊進行維護了,導致項目的 Swift 化和組件化 / 模塊化工作並沒有全部完成,這一點有些遺憾。不過還是希望蜂鳥商家版能夠越來越好,繼續為廣大商家朋友們服務。

好消息是,接下來我主要參與蜂鳥團隊版 App 的架構工作,這一次我們根據之前暴露出的問題制定了詳細的工作計劃,有了蜂鳥商家版的踩坑經驗後,我相信這一次我們一定能順利完成目標。2018,加油,一起拼!


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Cocoa開發者社區 的精彩文章:

耳機不僅是程序員工作中的好夥伴,而且它還有這樣一個隱蔽的功能……

TAG:Cocoa開發者社區 |