iOS用被誤解的MVC重構代碼

LinTrott 7年前發布 | 10K 次閱讀 MVC模式 iOS開發 移動開發

前言

這段時間在重構代碼,看了幾種模式,最后選擇使用被誤解的MVC來重構。

下面分別簡要介紹MVVM(RAC)、MVP、MVC模式,同時分享一下在重構代碼過程中的一些想法。

MVVM

  1. 優點:
  • 雙向綁定(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比較像

重構

在重構前先看幾個問題:

  1. 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。
  2. 是什么導致了massive viewcontroller?
    我的理解是因為沒有將MVC的各層職能分清,而把視圖、業務邏輯都往ViewController上堆,自然就成了massive viewcontroller。
  3. 如果使用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>

小結

重構后的優點:

  1. 各層職能變得更加清晰。
  2. View與Controller徹底解耦。(LoginController以接口形式調用視圖層,界面更改對其不產生影響,自身的修改也對視圖層不產生影響。)
  3. 代碼復用度高。(LoginController可復用于老師和學生的賬號登錄)
  4. 測試方便。(若要測試登錄接口是否可行,可直接實例化 LoginService調用登錄接口進行測試)
  5. 把視圖邏輯交于ViewController,業務邏輯交于Controller,解決了massive viewcontroller和視圖的datasource、delegate代碼放置位置等問題。
  6. 任務分配方便。(接口約定完畢后視圖層、控制層、模型層可以單獨由不同人完成)

缺點:

  1. 多了一些膠水代碼。
  2. 需要多定義視圖、模型的接口(CATLoginControllerDelegate)
  3. ...

最后

本文的分層方式并不一定適合每個工程,大家可以根據自己工程的情況自行調整。簡友【我在睡覺被占用】說得好,其實不用太拘泥與什么模式,去扣定義。只要遵循盡量解耦,關系邏輯清晰的原則就行了。在此表示感謝!

然而,可能只有我誤解了MVC。

 

來自:http://www.jianshu.com/p/02d0d12a1fa9

 

 本文由用戶 LinTrott 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!