iOS用被誤解的MVC重構代碼
前言
這段時間在重構代碼,看了幾種模式,最后選擇使用被誤解的MVC來重構。
下面分別簡要介紹MVVM(RAC)、MVP、MVC模式,同時分享一下在重構代碼過程中的一些想法。
MVVM
- 優點:
- 雙向綁定(data-binding):View的變動,自動反映在ViewModel,反之亦然。使用過Angular 和 Ember 的朋友應該對這點很熟悉。
- 使得 Model 層和 View 層解耦
- 結合RAC使用變得神乎其技。特別是面對 View與View之間變化關系緊密 時RAC能處理得很elegant。
- 解決了 狀態量 的問題(即無狀態)
MVVM
2.缺點:
- ViewModel承擔了大部分MVC中C的事務。【本質上沒有解決MVC的 massive viewcontroller 問題】
- 數據綁定使得 Bug 調試變難。【由于雙向綁定使得 View和Model的bug 較難定位】
- 數據綁定需要花費更多的內存。【這是個缺點,但項目實踐中我沒怎么發覺到】
- RAC學習成本較高。
3.總結:
MVVM是我最先考慮的模式,原因是被RAC吸引了。
MVVM不失為一個良好的模式,但其 缺點由其優點而來 ,使用過程中較難避免。
關于項目是否使用MVVM,我的觀點是:
- 如果團隊人員都能較好領會函數響應式編程思想、bug定位能力較強的話,可以使用。
-
如果項目的邏輯較為復雜導致狀態量較多時可以考慮使用。
我在業余作品中還是喜歡使用RAC的,在工作上沒有使用RAC原因是沒有很好的隊友,為了項目的可維護性而放棄了RAC。
MVP
MVP 是從MVC演變而來,它們有相通的地方:Controller/Presenter負責邏輯的處理,Model提供數據,View負責顯示。MVP與MVC有一個重大的區別:在MVP中View并不直接使用Model,它們之間的通信是通過Presenter (MVC中的Controller)來進行的,所有的交互都發生在Presenter內部,而在 MVC中View會直接從Model中讀取數據 而不是通過 Controller。
看到最后一句的時候相信大家都會有疑問,也許會指著下面這張斯坦福教授的圖說MVC的View和Model是沒有直接通訊的。
斯坦福MVC
但傳統的MVC并不是這樣的,百科 MVC框架
MVC圖1
MVC圖2
那么哪個才是真正的MVC?這也是今天主要想跟大家交流的,為了繼續這個話題我們先進一步了解MVP模式。
MVP
在MVP中View持有一個Presenter對象,View將界面的響應處理移交給Presenter,而Presenter調用Model進行處理,最后Presenter將Model處理完畢的數據通過Interface的形式遞交給View做相應的改變。
MV(X)本是同根生,自然有一些相同點。MVC在每一個平臺上都有自己的特點,自然也會稍許不同。所以,你也許會感覺 MVP才跟斯坦福教授講的MVC比較像 !
重構
在重構前先看幾個問題:
- iOS中的ViewController到底是MVC中的View還是Controller?還是有獨到的看法?
我在圈子里面做了一個訪談。總數53人,有21人答案是View,30人答案是Controller,2人有獨到的看法。當時我很驚訝!盡然對ViewController有這么多不同的看法。在此分享對此的一些看法,如有疏漏,望大家指正。
做過Android的朋友會發現ViewController與Android的Activity及其相似。我認為ViewController總體上屬于MVC中的View層,但與傳統的View不一樣的是ViewController附帶了一些Controller的邏輯,但該邏輯 僅為"視圖邏輯" (相對于"業務邏輯"而言)。我想這也是apple管它叫"視圖控制器"的原因。需要明白的一點是,apple造了一個ViewController,但它和MVC模式都沒有限制我們只能把它當做Controller,完全可以自定義一個Controller。 - 是什么導致了massive viewcontroller?
我的理解是因為沒有將MVC的各層職能分清,而把視圖、業務邏輯都往ViewController上堆,自然就成了massive viewcontroller。 - 如果使用MVVM,那么tableview的datasource&delegate應該放在哪里比較合適?如何解決這個問題?
我沒有答案,因為覺得放在MVVM中的哪一層都覺得不合適。望大神告知!
為了解決開發中的問題,我對MVC各層重新做了職能分配。
MVC
注:單獨箭頭表示直接引用,箭頭帶圓圈表示以接口引用。
重構后的分層模式與職能分配:
-
View層:由View與ViewController組成。View為單獨的視圖,ViewController負責多個視圖的管理、tableview的datasource & delegate等視圖邏輯(這也就解決了問題3)。ViewController會持有一個Controller來傳遞視圖需要響應的業務邏輯。
-
Controller層:負責業務邏輯的處理。Controller持有View和Service的接口引用(Service可根據項目特點選擇直接/接口引用)。Controller通過調用Service來處理View層傳遞下來的業務,并用接口引用遞交結果給View層做相應的改變。
-
Model層:由Service與Entity組成。Service為Controller層提供網絡與本地數據服務,即Service處理網絡請求、數據庫、文件等操作。Entity為實體類,負責定義數據的模型。
Show me the code
先說明一下code的場景:
code為一個登錄模塊,賬號類型分老師和學生,并且老師和學生的登錄界面不同,但接口調用一致。
Model層代碼
Entity
@interface CATUserEntity : NSObject
@property (nonatomic,copy) NSString username;
@property (nonatomic,copy) NSString gender;
@property (nonatomic) NSInteger age;
@end</code></pre>
Service
@interface CATLoginService : CATBaseService
-(void)loginWithUsername:(NSString )username password:(NSString )password type:(NSInteger)type success:(CATSuccessBlock)success failed:(CATFailedBlock)failed;
@end
@implementation CATLoginService
-(void)loginWithUsername:(NSString )username password:(NSString )password type:(NSInteger)type success:(CATSuccessBlock)success failed:(CATFailedBlock)failed{
//在這里調用網絡、操作數據庫等
//返回數據并解析成相應的數據,這里模擬返回一個User的實體。
//網絡層這里推薦 巧哥使用命令模式封裝的YTKNetworking!!!
CATUserEntity* user = [[CATUserEntity alloc]init];
user.gender = @"男";
user.age = 20;
if (type == 1) {
user.username = @"老師";
}else{
user.username = @"學生";
}
success(@"登錄成功!",user);
}
@end</code></pre>
Controller層代碼(由于項目特點,這里的Model沒有以接口形式引用)
@protocol CATLoginControllerDelegate <NSObject>
-(void)loginSuccessWithData:(id)data;
-(void)loginFailedWithMsg:(NSString *)msg;
@end
@interface CATLoginController : NSObject
-(id)initWith:(id<CATLoginControllerDelegate>)delegate;
-(void)loginWithUsername:(NSString )username password:(NSString )password type:(NSInteger)type;
@end
@interface CATLoginController()
@property (nonatomic,weak) id<CATLoginControllerDelegate> delegate;
@property (nonatomic,strong) CATLoginService* service;
@end
@implementation CATLoginController
-(id)initWith:(id<CATLoginControllerDelegate>)delegate{
self = [super init];
if (self) {
_delegate = delegate;
}
return self;
}
-(void)loginWithUsername:(NSString )username password:(NSString )passwor type:(NSInteger)type{
WEAKSELF
[self.service loginWithUsername:username password:passwor type:type success:^(NSString msg, id data) {
STRONGSELF
if (data && strongSelf.delegate && [strongSelf.delegate respondsToSelector:@selector(loginSuccessWithData:)]){//登錄成功 && delegate實現了相應的方法
[strongSelf.delegate loginSuccessWithData:data];
}else if(strongSelf.delegate && [strongSelf.delegate respondsToSelector:@selector(loginFailedWithMsg:)]){//登錄失敗 && delegate實現了相應的方法
[strongSelf.delegate loginFailedWithMsg:msg];
}else{
//handle...
}
} failed:^(NSString msg) {
//handle error
}];
}
- (CATLoginService *) service {
if(!_service) {
_service = [[CATLoginService alloc] init];
}
return _service;
}
@end</code></pre>
View層代碼
老師登錄界面
@interface CATTeacherLoginViewController ()<CATLoginControllerDelegate>
@property (nonatomic,strong) CATLoginController controller;
@property (weak, nonatomic) IBOutlet UILabel labMsg;
@end
@implementation CATTeacherLoginViewController
(void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.title = @"老師登錄界面";
}
(IBAction)loginButtonClicked:(id)sender {
[self.controller loginWithUsername:@"111" password:@"111" type:1];
}
(CATLoginController *) controller {
if(_controller == nil) {
_controller = [[CATLoginController alloc] initWith:self];
}
return _controller;
}
-(void)loginSuccessWithData:(id)data{
//處理登錄成功后的界面呈現
if (data && [data isKindOfClass:[CATUserEntity class]]) {
CATUserEntity user = (CATUserEntity )data;
_labMsg.text = [NSString stringWithFormat:@"登錄成功!你好:%@",user.username];
}
}
-(void)loginFailedWithMsg:(NSString *)msg{
//處理登錄失敗后的界面呈現
NSLog(@"登錄失敗:%@",msg);
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
}
@end</code></pre>
學生登錄界面
@interface CATStudentLoginViewController ()<CATLoginControllerDelegate>
@property (nonatomic,strong) CATLoginController controller;
@property (weak, nonatomic) IBOutlet UILabel labMsg;
@end
@implementation CATStudentLoginViewController
(void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.title = @"學生登錄界面";
}
(IBAction)loginButtonClicked:(id)sender {
[self.controller loginWithUsername:@"111" password:@"111" type:2];
}
(CATLoginController *) controller {
if(_controller == nil) {
_controller = [[CATLoginController alloc] initWith:self];
}
return _controller;
}
-(void)loginSuccessWithData:(id)data{
//處理登錄成功后的界面呈現
if (data && [data isKindOfClass:[CATUserEntity class]]) {
CATUserEntity user = (CATUserEntity )data;
_labMsg.text = [NSString stringWithFormat:@"登錄成功!你好:%@",user.username];
}
}
-(void)loginFailedWithMsg:(NSString *)msg{
//處理登錄失敗后的界面呈現
NSLog(@"登錄失敗:%@",msg);
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
}
@end</code></pre>
小結
重構后的優點:
- 各層職能變得更加清晰。
- View與Controller徹底解耦。(LoginController以接口形式調用視圖層,界面更改對其不產生影響,自身的修改也對視圖層不產生影響。)
- 代碼復用度高。(LoginController可復用于老師和學生的賬號登錄)
- 測試方便。(若要測試登錄接口是否可行,可直接實例化 LoginService調用登錄接口進行測試)
- 把視圖邏輯交于ViewController,業務邏輯交于Controller,解決了massive viewcontroller和視圖的datasource、delegate代碼放置位置等問題。
- 任務分配方便。(接口約定完畢后視圖層、控制層、模型層可以單獨由不同人完成)
缺點:
- 多了一些膠水代碼。
- 需要多定義視圖、模型的接口(CATLoginControllerDelegate)
- ...
最后
本文的分層方式并不一定適合每個工程,大家可以根據自己工程的情況自行調整。簡友【我在睡覺被占用】說得好,其實不用太拘泥與什么模式,去扣定義。只要遵循盡量解耦,關系邏輯清晰的原則就行了。在此表示感謝!
然而,可能只有我誤解了MVC。
來自:http://www.jianshu.com/p/02d0d12a1fa9