新入職了被盈,前一個(gè)月陸陸續(xù)續(xù)把之前一個(gè)App重構(gòu)了一下下炸枣,目前重構(gòu)了一半,基本架構(gòu)算是弄完了民镜,先總結(jié)下啡专,后面接著完善。
分以下說(shuō)明下:1: 為什么要重構(gòu)
2:重構(gòu)前的準(zhǔn)備工作
3:重構(gòu)之路
1:為什么要重構(gòu)
1.1 代碼層面
首先看到我司這個(gè)新的App之前寫(xiě)的代碼制圈,是用 MVC
模式寫(xiě)的植旧,但是發(fā)現(xiàn)一個(gè)類(lèi)里面尤其是viewcontroller
寫(xiě)了幾百行代碼,有的都是一千多行,我熟悉起來(lái)感覺(jué)好累离唐,其次模塊架構(gòu)分的還是不夠清晰病附,有些業(yè)務(wù)邏輯放到了其他模塊里面,沒(méi)有單獨(dú)拆分出來(lái)亥鬓。最后感覺(jué)里面冗余了很多無(wú)用的代碼完沪。當(dāng)然了我的吐槽并不是說(shuō)我自己多牛,其實(shí)我也只是個(gè)菜鳥(niǎo)嵌戈,只是希望重構(gòu)一下方便后續(xù)維護(hù)覆积。
1.2 個(gè)人原因
因?yàn)槲抑岸际亲鯯DK開(kāi)發(fā),很久沒(méi)有單獨(dú)開(kāi)發(fā)App了熟呛,并且之前的公司代碼全部都是模塊化管理宽档,我們現(xiàn)在這個(gè)完整的App卻是耦合性很高,加之我反復(fù)熟悉了我司之前的這個(gè)App業(yè)務(wù)邏輯庵朝,前段時(shí)間也比較空閑吗冤,于是就決定重構(gòu)一下。
2.重構(gòu)前的準(zhǔn)備工作
根據(jù)我之前的經(jīng)驗(yàn)九府,我希望這個(gè)App模塊化椎瘟,耦合性盡量低一些。
2.1 設(shè)計(jì)模式的轉(zhuǎn)變
我準(zhǔn)備從之前的單純
mvc
設(shè)計(jì)模式轉(zhuǎn)變成MVVM With ReactiveCocoa設(shè)計(jì)模式
,
MVVM的使用我參考了以下文章侄旬,供大家參考:
iOS 關(guān)于MVC和MVVM設(shè)計(jì)模式的那些事
對(duì)于結(jié)合使用ReactiveCocoa
,我覺(jué)得可以更加方便view
層和ViewModel
層之間的交互肺蔚,并且ReactiveCocoa
為事件提供了很多處理方法,而且利用RAC處理事件很方便儡羔,可以把要處理的事情宣羊,和監(jiān)聽(tīng)的事情的代碼放在一起,這樣非常方便我們管理汰蜘,就不需要跳到對(duì)應(yīng)的方法里仇冯。非常符合我們開(kāi)發(fā)中高聚合,低耦合
的思想鉴扫。對(duì)于它的使用我參考了一下文章:
最快讓你上手ReactiveCocoa之基礎(chǔ)篇
ReactiveCocoa 中 RACSignal 是如何發(fā)送信號(hào)的
2.2 組件化改造
組件化我直接使用Casa的 iOS應(yīng)用架構(gòu)談 組件化方案,簡(jiǎn)單來(lái)說(shuō)基于casa的 CTMediator
組件架構(gòu)內(nèi)部調(diào)用部分
通過(guò)Target+Action以及組件+類(lèi)別的調(diào)用方式組成一個(gè)中介者模式赞枕。
原因很簡(jiǎn)單我之前的公司就是使用這一套架構(gòu),我覺(jué)得還不錯(cuò)坪创,我這邊就接著借鑒使用了炕婶。
其實(shí)組件化有很多方案,強(qiáng)烈推薦可以參考這篇文章
3.重構(gòu)之路
3.1項(xiàng)目結(jié)構(gòu)整理
首先大概看下這個(gè)App結(jié)構(gòu)
從這個(gè)效果圖上面我先總結(jié)了重構(gòu)該項(xiàng)目大概所需要的組件
3.2 MVVM+RAC項(xiàng)目重構(gòu)開(kāi)始
-
之前項(xiàng)目中全部是本地文件放到具體項(xiàng)目中莱预,沒(méi)有使用
cooapods
柠掂,我們代碼目前還是托管到svn
上。我想進(jìn)行模塊化依沮,每個(gè)不同功能的組件都能進(jìn)行cocoapods
管理涯贞,考慮到代碼私有化,我暫時(shí)沒(méi)有將要重構(gòu)的代碼托管到第三方的git
倉(cāng)庫(kù)上危喉,先將代碼放到本地指定文件下宋渔,每個(gè)模塊也加入podspec
文件,通過(guò)引用本地路徑辜限,同樣也能實(shí)現(xiàn)cocoaPods
管理皇拣,每個(gè)模塊文件都可以pod
下載管理。
使用本地cocapods
管理之后薄嫡,podfie
文件里面管理大概就是類(lèi)似下面這個(gè)模塊了:
這樣重構(gòu)的項(xiàng)目模塊依賴(lài)只需要管理對(duì)應(yīng)的podfile
文件就好了氧急。 -
由于資訊列表頁(yè)面就是請(qǐng)求數(shù)據(jù)和解析相關(guān)數(shù)據(jù),無(wú)需其他業(yè)務(wù)邏輯毫深,首先我重構(gòu)該模塊吩坝。
就以這個(gè)模塊為例說(shuō)明下:
-
使用MVVM我們離不開(kāi)
View
,ViewModel
哑蔫,Controller
钉寝,
這里先統(tǒng)一定義下baseView
,baseViewModel
闸迷,baseController
瘩蚪,
首先建立BaseViewmodel
,遵循BaseViewModelProtocol
協(xié)議。
@protocol PLBaseViewModelProtocol <NSObject>
@optional
@property (nonatomic, readonly, copy) NSDictionary *params;
@property (nonatomic, readonly, copy) NSString *title;
//error接受者
@property (nonatomic, readonly, strong) RACSubject *errors;
- (instancetype)initWithParams:(NSDictionary *)params;
- (void)initialize;
@end
所有的viewmodel
都要繼承它BaseViewmodel
稿黍。
BaseViewmodel
里面結(jié)構(gòu)如下:
通過(guò)
- (instancetype)initWithParams:(NSDictionary *)params
方法傳遞進(jìn)來(lái)一些基本參數(shù)提供給Viewmodel
使用,在initialize
方法里面子類(lèi)里面放些需要初始化的操作疹瘦。建立
PLBaseView
,遵循PLBaseViewProtocol
,所有直接繼承UIView
類(lèi)型的view都要繼承它,其他view比如tableViewcell
遵循PLBaseViewProtocol
。
@protocol PLBaseViewProtocol <NSObject>
@optional
@property (nonatomic, strong, readonly) PLBaseViewModel *viewModel;
- (instancetype)initWithViewModel:(PLBaseViewModel *)viewModel;
- (void)renderViews;
- (void)bindViewModel:(id)viewModel;
- (void)bindViewModel;
@end
@implementation PLBaseView
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
PLBaseView *view = [super allocWithZone:zone];
@weakify(view)
[[view rac_signalForSelector:@selector(initWithViewModel:)] subscribeNext:^(RACTuple * _Nullable x) {
@strongify(view)
[view renderViews];
[view bindViewModel];
}];
return view;
}
- (instancetype)initWithViewModel:(PLBaseViewModel *)viewModel {
if (self = [super init]) {
_viewModel = viewModel;
}
return self;
}
//配置子視圖的操作放在這個(gè)地方
- (void)renderViews {
}
//與viewModel的具體交互操作
- (void)bindViewModel {
}
- (void)bindViewModel:(id)viewModel {
_viewModel = viewModel;
}
建立baseControllerView
,其實(shí)這個(gè)類(lèi)里面類(lèi)容和baseView
里面類(lèi)似巡球,綁定viewModel
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
PLModelViewController *viewController = [super allocWithZone: zone];
@weakify(viewController)
[[viewController rac_signalForSelector:@selector(viewDidLoad)] subscribeNext:^(RACTuple * _Nullable x) {
@strongify(viewController)
[viewController bindViewModel];
}];
return viewController;
}
- (instancetype)initWithViewModel:(PLBaseViewModel *)viewModel {
if (self = [super init]) {
self.viewModel = viewModel;
}
return self;
}
//通過(guò)RAC進(jìn)行title綁定操作
- (void)bindViewModel {
RAC(self,title) = RACObserve(self, viewModel.title);
//訂閱信號(hào)
[self.viewModel.errors subscribeNext:^(NSError *error) {
NSLog(@"viewModel 錯(cuò)誤信息------%@",error);
}];
}
還有在通過(guò)繼承baseViewController
和baseView
分別針對(duì)tabelView做了進(jìn)一步處理言沐,新建類(lèi)PLBaseTableViewController
,PLBaseTableViewModel
,這里代碼代碼就不在詳細(xì)贅述。
PLBaseTableViewModel
主要做了如下額外處理:
PLBaseTableViewController
主要做了如下處理酣栈,主要添加了tabelView
,集成了MJRefresh
下拉刷新和上拉加載功能.- 建立
UITableViewDataSource
的代理類(lèi)TabelViewArrayDataSource
,
將UITableViewDataSource
相關(guān)代理方法分離出去
這樣的好處就是避免使用tabelveVIew的時(shí)候還導(dǎo)入UITableViewDataSource
,降低了代碼的耦合性险胰。
上面主要的基類(lèi)創(chuàng)建完成,就可以開(kāi)始使用MVVM+RAC正式開(kāi)始構(gòu)建業(yè)務(wù)相關(guān)頁(yè)面了矿筝。
- 針對(duì)
資訊
新聞列表頁(yè)面創(chuàng)建FitfunInfoListViewModel
,繼承于PLBaseTableViewModel
,所有的網(wǎng)絡(luò)交換和數(shù)據(jù)獲取都是放在viewModel
中的起便。這里簡(jiǎn)單說(shuō)下:
在.h
文件
@class FitfunBannerModel;
@interface FitfunInfoListViewModel : PLBaseTableViewModel
//滾動(dòng)視圖數(shù)據(jù)源
@property (nonatomic, readonly, strong) NSArray <FitfunBannerModel *> *banners;
//請(qǐng)求banner數(shù)據(jù)命令
@property (nonatomic, readonly, strong) RACCommand *requestBannerDataCommand;
@end
主要就是通過(guò)RAC信號(hào)源
來(lái)進(jìn)行數(shù)據(jù)交換和傳遞
在.m
文件里面主要用到了RAC的RACCommand
,
RACCommand
:RAC中用于處理事件的類(lèi),可以把事件如何處理,事件中的數(shù)據(jù)如何傳遞榆综,包裝到這個(gè)類(lèi)中妙痹,他可以很方便的監(jiān)控事件的執(zhí)行過(guò)程。具體使用不熟悉的可以參考上面【設(shè)計(jì)模式的轉(zhuǎn)變】我分享RAC學(xué)習(xí)的鏈接鼻疮。
使用場(chǎng)景:監(jiān)聽(tīng)按鈕點(diǎn)擊怯伊,網(wǎng)絡(luò)請(qǐng)求
網(wǎng)絡(luò)請(qǐng)求方面我還進(jìn)行了解耦。
使用的是casa的那一套iOS應(yīng)用架構(gòu)談 網(wǎng)絡(luò)層設(shè)計(jì)方案,簡(jiǎn)單來(lái)說(shuō)是使用CTNetworking判沟,網(wǎng)絡(luò)層上部分使用離散型設(shè)計(jì)耿芹,下部分使用集約型設(shè)計(jì),使用delegate來(lái)做數(shù)據(jù)對(duì)接挪哄,僅在必要時(shí)采用Notification來(lái)做跨層訪問(wèn)吧秕,設(shè)計(jì)合理的繼承機(jī)制,讓派生出來(lái)的APIManager受到限制迹炼。這個(gè)具體有興趣了解的話·請(qǐng)看上面共享的鏈接砸彬。
在FitfunInfoListViewModel.m
文件里面我具體的體現(xiàn)是遵循CTAPIManagerParamSource,CTAPIManagerCallBackDelegate
協(xié)議,然后再對(duì)應(yīng)協(xié)議里面?zhèn)鬟f需要的參數(shù)和相應(yīng)對(duì)應(yīng)的請(qǐng)求結(jié)果疗涉。進(jìn)行網(wǎng)絡(luò)請(qǐng)求就需要我們繼承CTAPIBaseManager
,里面設(shè)置一些網(wǎng)絡(luò)請(qǐng)求相關(guān)操作拿霉□尾妫看下里面我寫(xiě)的大致代碼:
#pragma mark - CTAPIManagerParamSource
//這里放額外拼接的參數(shù)
- (NSDictionary *)paramsForApi:(CTAPIBaseManager *)manager {
NSMutableDictionary *dic = [[NSMutableDictionary alloc]init];
if (manager == self.bannerImageAPIManager) {
dic[@"id"] = (self.params[@"bannerID"]?:@"");
}
return dic;
}
#pragma mark -CTAPIManagerCallBackDelegate
//這里網(wǎng)絡(luò)請(qǐng)求成功的相關(guān)操作
- (void)managerCallAPIDidSuccess:(CTAPIBaseManager *)manager {
}
//網(wǎng)絡(luò)請(qǐng)求失敗相關(guān)操作
- (void)managerCallAPIDidFailed:(CTAPIBaseManager *)manager{
}
#pragma mark - getter&&setter
//繼承于CTAPIBaseManager迎捺,這里配置網(wǎng)絡(luò)請(qǐng)求地址和請(qǐng)求類(lèi)型
- (FitfunAPIBaseManager *)topicInfoAPIManager {
if (!_topicInfoAPIManager) {
_topicInfoAPIManager = [[FitfunAPIBaseManager alloc] initWithMethodName:front_content_list reuquest:CTAPIManagerRequestTypePost];
_topicInfoAPIManager.paramSource = self;
_topicInfoAPIManager.delegate = self;
}
return _topicInfoAPIManager;
}
//reformer遵循`CTAPIManagerDataReformer`,用來(lái)統(tǒng)一解析網(wǎng)絡(luò)請(qǐng)求成功數(shù)據(jù)
- (FitfunAPIBaseDataReformer *)reformer {
if (!_reformer) {
_reformer = [[FitfunAPIBaseDataReformer alloc]init];
}
return _reformer;
}
資訊的新聞列表的ViewModel
寫(xiě)完了,就可以新建ViewFitfunInfoListTableViewCell
,遵循PLBaseViewProtocol
協(xié)議,
view里面主要就是進(jìn)行視圖搭建和解析ViewModel相關(guān)操作测萎,這里直接跳過(guò)闹伪。
然后新建新聞列表的FitfunInfoViewController
繼承于PLBaseTableViewController
,通過(guò)FitfunInfoListViewModel
進(jìn)行數(shù)據(jù)關(guān)聯(lián)沪铭,RAC進(jìn)行數(shù)據(jù)傳遞,最后我們的新聞列表Controller里面結(jié)構(gòu)和代碼就會(huì)特別清爽偏瓤。
我只需要在綁定的ViewModel
對(duì)應(yīng)的數(shù)據(jù)監(jiān)聽(tīng)方法中操作就ok了杀怠。
大致重構(gòu)項(xiàng)目使用
MVVM+RAC
的簡(jiǎn)單說(shuō)明就是這樣了,重新調(diào)整了下結(jié)構(gòu)厅克,使業(yè)務(wù)處理邏輯更加清晰赔退,代碼結(jié)構(gòu)慢慢趨于完善。
為了更直觀看出代碼的變化我們可以對(duì)比下新聞列表頁(yè)構(gòu)造前后代碼的變化和調(diào)整:
3.3 AppDelegate解耦
初步改造之后证舟,然后看了下之前項(xiàng)目里面AppDelegate各種注冊(cè)和事件處理硕旗,Appdelegate.m
代碼冗余度太高了。于是考慮到如何將Appdelegate解耦下女责。綜合之前工作經(jīng)驗(yàn)和網(wǎng)上別人說(shuō)的漆枚,我總結(jié)了目前有大致如下幾種解耦方式:
第一個(gè)當(dāng)然使我們最熟悉的使用AppDelegate 分類(lèi) (Category),
創(chuàng)建 AppDelegate 分類(lèi)無(wú)疑是低投入高產(chǎn)出的最好解決方案了FRDModuleManager,FRDModuleManager 是豆瓣開(kāi)源的輕量級(jí)模塊管理工具,其內(nèi)部數(shù)組持有注冊(cè)的模塊的引用抵知,通過(guò)依次調(diào)用數(shù)組中的每個(gè)模塊的特定方法來(lái)達(dá)到解耦的目的.
JSDecoupledAppDelegate,通過(guò)轉(zhuǎn)發(fā) AppDelegate 的各個(gè)方法來(lái)實(shí)現(xiàn) AppDelegate 的解耦的:
JLRoutes
MGJRouter
靈活的 iOS URL Router,由于這個(gè)我沒(méi)有用過(guò)墙基,不是很熟悉软族,感興趣可自尋了解下
這里還增加兩個(gè)我之前公司用到的另外兩個(gè)解耦方法
Aspects,一個(gè)輕量級(jí)的面向切面編程的庫(kù)。它能允許你在每一個(gè)類(lèi)和每一個(gè)實(shí)例中存在的方法里面加入任何代碼,通過(guò)Runtime消息轉(zhuǎn)發(fā)實(shí)現(xiàn)Hook,我們可以使用它在各個(gè)類(lèi)里面攔截
AppDelegate
的各個(gè)方法残制。這個(gè)代碼完全無(wú)侵入立砸,之前公司一直在用,使用過(guò)程中就是偶爾會(huì)發(fā)生莫名的攔截錯(cuò)誤痘拆。還不錯(cuò)仰禽,感興趣的可參考大神的iOS 如何實(shí)現(xiàn)Aspect Oriented ProgrammingBeeHive,BeeHive是阿里用于iOS的App模塊化編程的框架實(shí)現(xiàn)方案氮墨,吸收了Spring框架Service的理念來(lái)實(shí)現(xiàn)模塊間的API耦合纺蛆。我之前公司用于游戲代理層方面的解耦,使用起來(lái)非常不錯(cuò)的一個(gè)框架规揪。如果對(duì)這個(gè)框架感興趣桥氏,可以參考大神關(guān)于這個(gè)框架源碼解析BeeHive —— 一個(gè)優(yōu)雅但還在完善中的解耦框架
我這個(gè)項(xiàng)目中考慮了一下,我重構(gòu)的項(xiàng)目目前不是很大猛铅,在Appdelegate
對(duì)應(yīng)方法里面注冊(cè)的類(lèi)相對(duì)有限字支,我想在各個(gè)類(lèi)里面對(duì)應(yīng)響應(yīng)UIApplicationDelegate
協(xié)議方法,為了簡(jiǎn)單化一些暫時(shí)參考了FRDModuleManager奸忽,在這個(gè)基礎(chǔ)上簡(jiǎn)單改造了成了PLModuleManager
堕伪,使用時(shí)需要在Appdelegate.m
使用PLModuleManager
注冊(cè)相關(guān)方法,在PLModuleManager
初始化時(shí)候持有需要注冊(cè)類(lèi)
栗菜,而注冊(cè)過(guò)的類(lèi)
遵循UIApplicationDelegate
和UNUserNotificationCenterDelegate
協(xié)議欠雌,實(shí)現(xiàn)需要的協(xié)議方法即可。
后續(xù)代碼調(diào)整和總結(jié):
初步重構(gòu)項(xiàng)目的架構(gòu)就是那樣了疙筹,后面還需要做的就是
- 把原來(lái)項(xiàng)目中到處隨意使用的通知
NSNotificationCenter
全部替換掉富俄,用block
和delegate
等形式。亂用通知會(huì)導(dǎo)致代碼不可控而咆,管理性極差霍比。 - 代碼格式統(tǒng)一規(guī)范,代碼風(fēng)格要按照標(biāo)準(zhǔn)書(shū)寫(xiě)暴备,自己現(xiàn)在重構(gòu)的項(xiàng)目偷懶寫(xiě)的一些不合理的地方也得糾正過(guò)來(lái)悠瞬。還有盡量使用代碼構(gòu)建視圖,代替之前項(xiàng)目中大量使用
xib
,xib
的過(guò)多使用會(huì)導(dǎo)致代碼不好維護(hù)涯捻,尤其后面多人維護(hù)的時(shí)候浅妆。 - 把iOS環(huán)信通信這塊代碼重新整合一下,還有對(duì)現(xiàn)在對(duì)
MVVM+RAC
的使用不夠熟練汰瘫,這一塊有時(shí)間重新優(yōu)化改造下狂打。 - 其他的暫時(shí)沒(méi)有想到的等以后想起來(lái)或發(fā)現(xiàn)糾正。
疏漏和不合理之處如果各位哥哥姐姐們看到了混弥,請(qǐng)不吝賜教趴乡,我只是按照自己的思路簡(jiǎn)單初步總結(jié)了下后續(xù)重構(gòu)完畢這部分文章重新再整理下.
如果有需要对省,可參考我根據(jù)上面思路初步寫(xiě)的Demo