從簡書遷移到掘金...
前言
本文為回答一位朋友關於MVC/MVP/MVVM架構方面的疑問所寫, 旨在介紹iOS下MVC/MVP/MVVM三種架構的設計思路以及各自的優缺點. 全文約五千字, 預計花費閱讀時間20 - 30分鐘.
MVC
- MVC的相關概念 MVC最早存在於桌面程式中的, M是指業務資料, V是指使用者介面, C則是控制器. 在具體的業務場景中, C作為M和V之間的連線, 負責獲取輸入的業務資料, 然後將處理後的資料輸出到介面上做相應展示, 另外, 在資料有所更新時, C還需要及時提交相應更新到介面展示. 在上述過程中, 因為M和V之間是完全隔離的, 所以在業務場景切換時, 通常只需要替換相應的C, 複用已有的M和V便可快速搭建新的業務場景. MVC因其複用性, 大大提高了開發效率, 現已被廣泛應用在各端開發中.
概念過完了, 下面來看看, 在具體的業務場景中MVC/MVP/MVVM都是如何表現的.
- MVC之消失的C層
//UserVC
- (void)viewDidLoad {
[super viewDidLoad];
[[UserApi new] fetchUserInfoWithUserId:132 completionHandler:^(NSError *error, id result) {
if (error) {
[self showToastWithText:@"獲取使用者資訊失敗了~"];
} else {
self.userIconIV.image = ...
self.userSummaryLabel.text = ...
...
}
}];
[[userApi new] fetchUserBlogsWithUserId:132 completionHandler:^(NSError *error, id result) {
if (error) {
[self showErrorInView:self.tableView info:...];
} else {
[self.blogs addObjectsFromArray:result];
[self.tableView reloadData];
}
}];
}
//...略
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
BlogCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BlogCell"];
cell.blog = self.blogs[indexPath.row];
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[self.navigationController pushViewController:[BlogDetailViewController instanceWithBlog:self.blogs[indexPath.row]] animated:YES];
}
//...略
複製程式碼
//BlogCell
- (void)setBlog:(Blog)blog {
_blog = blog;
self.authorLabel.text = blog.blogAuthor;
self.likeLebel.text = [NSString stringWithFormat:@"贊 %ld", blog.blogLikeCount];
...
}
複製程式碼
程式設計師很快寫完了程式碼, Command+R一跑, 沒有問題, 心滿意足的做其他事情去了. 後來有一天, 產品要求這個業務需要改動, 使用者在看他人資訊時是上圖中的頁面, 看自己的資訊時, 多一個草稿箱的展示, 像這樣:
於是小白將程式碼改成這樣://UserVC
- (void)viewDidLoad {
[super viewDidLoad];
if (self.userId != LoginUserId) {
self.switchButton.hidden = self.draftTableView.hidden = YES;
self.blogTableView.frame = ...
}
[[UserApi new] fetchUserI......略
[[UserApi new] fetchUserBlogsWithUserId:132 completionHandler:^(NSError *error, id result) {
//if Error...略
[self.blogs addObjectsFromArray:result];
[self.blogTableView reloadData];
}];
[[userApi new] fetchUserDraftsWithUserId:132 completionHandler:^(NSError *error, id result) {
//if Error...略
[self.drafts addObjectsFromArray:result];
[self.draftTableView reloadData];
}];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return tableView == self.blogTableView ? self.blogs.count : self.drafts.count;
}
//...略
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (tableView == self.blogTableView) {
BlogCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BlogCell"];
cell.blog = self.blogs[indexPath.row];
return cell;
} else {
DraftCell *cell = [tableView dequeueReusableCellWithIdentifier:@"DraftCell"];
cell.draft = self.drafts[indexPath.row];
return cell;
}
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (tableView == self.blogTableView) ...
}
//...略
複製程式碼
//DraftCell
- (void)setDraft:(draft)draft {
_draft = draft;
self.draftEditDate = ...
}
//BlogCell
- (void)setBlog:(Blog)blog {
...同上
}
複製程式碼
後來啊, 產品覺得使用者看自己的頁面再加個回收站什麼的會很好, 於是程式設計師又加上一段程式碼邏輯 , 再後來... 隨著需求的變更, UserVC變得越來越臃腫, 越來越難以維護, 擴充性和測試性也極差. 程式設計師也發現好像程式碼寫得有些問題, 但是問題具體出在哪裡? 難道這不是MVC嗎? 我們將上面的過程用一張圖來表示:
通過這張圖可以發現, 使用者資訊頁面作為業務場景Scene需要展示多種資料M(Blog/Draft/UserInfo), 所以對應的有多個View(blogTableView/draftTableView/image...), 但是, 每個MV之間並沒有一個連線層C, 本來應該分散到各個C層處理的邏輯全部被打包丟到了Scene這一個地方處理, 也就是M-C-V變成了MM...-Scene-...VV, C層就這樣莫名其妙的消失了.另外, 作為V的兩個cell直接耦合了M(blog/draft), 這意味著這兩個V的輸入被綁死到了相應的M上, 複用無從談起. 最後, 針對這個業務場景的測試異常麻煩, 因為業務初始化和銷燬被繫結到了VC的生命週期上, 而相應的邏輯也關聯到了和View的點選事件, 測試只能Command+R, 點點點...
- 正確的MVC使用姿勢
也許是UIViewController的類名給新人帶來了迷惑, 讓人誤以為VC就一定是MVC中的C層, 又或許是Button, Label之類的View太過簡單完全不需要一個C層來配合, 總之, 我工作以來經歷的專案中見過太多這樣的"MVC". 那麼, 什麼才是正確的MVC使用姿勢呢? 仍以上面的業務場景舉例, 正確的MVC應該是這個樣子的:
UserVC作為業務場景, 需要展示三種資料, 對應的就有三個MVC, 這三個MVC負責各自模組的資料獲取, 資料處理和資料展示, 而UserVC需要做的就是配置好這三個MVC, 並在合適的時機通知各自的C層進行資料獲取, 各個C層拿到資料後進行相應處理, 處理完成後渲染到各自的View上, UserVC最後將已經渲染好的各個View進行佈局即可, 具體到程式碼中如下:@interface BlogTableViewHelper : NSObject<UITableViewDelegate, UITableViewDataSource>
+ (instancetype)helperWithTableView:(UITableView *)tableView userId:(NSUInteger)userId;
- (void)fetchDataWithCompletionHandler:(NetworkTaskCompletionHander)completionHander;
- (void)setVCGenerator:(ViewControllerGenerator)VCGenerator;
@end
複製程式碼
@interface BlogTableViewHelper()
@property (weak, nonatomic) UITableView *tableView;
@property (copy, nonatomic) ViewControllerGenerator VCGenerator;
@property (assign, nonatomic) NSUInteger userId;
@property (strong, nonatomic) NSMutableArray *blogs;
@property (strong, nonatomic) UserAPIManager *apiManager;
@end
#define BlogCellReuseIdentifier @"BlogCell"
@implementation BlogTableViewHelper
+ (instancetype)helperWithTableView:(UITableView *)tableView userId:(NSUInteger)userId {
return [[BlogTableViewHelper alloc] initWithTableView:tableView userId:userId];
}
- (instancetype)initWithTableView:(UITableView *)tableView userId:(NSUInteger)userId {
if (self = [super init]) {
self.userId = userId;
tableView.delegate = self;
tableView.dataSource = self;
self.apiManager = [UserAPIManager new];
self.tableView = tableView;
__weak typeof(self) weakSelf = self;
[tableView registerClass:[BlogCell class] forCellReuseIdentifier:BlogCellReuseIdentifier];
tableView.header = [MJRefreshAnimationHeader headerWithRefreshingBlock:^{//下拉重新整理
[weakSelf.apiManage refreshUserBlogsWithUserId:userId completionHandler:^(NSError *error, id result) {
//...略
}];
}];
tableView.footer = [MJRefreshAnimationFooter headerWithRefreshingBlock:^{//上拉載入
[weakSelf.apiManage loadMoreUserBlogsWithUserId:userId completionHandler:^(NSError *error, id result) {
//...略
}];
}];
}
return self;
}
#pragma mark - UITableViewDataSource && Delegate
//...略
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.blogs.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
BlogCell *cell = [tableView dequeueReusableCellWithIdentifier:BlogCellReuseIdentifier];
BlogCellHelper *cellHelper = self.blogs[indexPath.row];
if (!cell.didLikeHandler) {
__weak typeof(cell) weakCell = cell;
[cell setDidLikeHandler:^{
cellHelper.likeCount += 1;
weakCell.likeCountText = cellHelper.likeCountText;
}];
}
cell.authorText = cellHelper.authorText;
//...各種設定
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[self.navigationController pushViewController:self.VCGenerator(self.blogs[indexPath.row]) animated:YES];
}
#pragma mark - Utils
- (void)fetchDataWithCompletionHandler:(NetworkTaskCompletionHander)completionHander {
[[UserAPIManager new] refreshUserBlogsWithUserId:self.userId completionHandler:^(NSError *error, id result) {
if (error) {
[self showErrorInView:self.tableView info:error.domain];
} else {
for (Blog *blog in result) {
[self.blogs addObject:[BlogCellHelper helperWithBlog:blog]];
}
[self.tableView reloadData];
}
completionHandler ? completionHandler(error, result) : nil;
}];
}
//...略
@end
複製程式碼
@implementation BlogCell
//...略
- (void)onClickLikeButton:(UIButton *)sender {
[[UserAPIManager new] likeBlogWithBlogId:self.blogId userId:self.userId completionHandler:^(NSError *error, id result) {
if (error) {
//do error
} else {
//do success
self.didLikeHandler ? self.didLikeHandler() : nil;
}
}];
}
@end
複製程式碼
@implementation BlogCellHelper
- (NSString *)likeCountText {
return [NSString stringWithFormat:@"贊 %ld", self.blog.likeCount];
}
//...略
- (NSString *)authorText {
return [NSString stringWithFormat:@"作者姓名: %@", self.blog.authorName];
}
@end
複製程式碼
Blog模組由BlogTableViewHelper(C), BlogTableView(V), Blogs(C)構成, 這裡有點特殊, blogs裡面裝的不是M, 而是Cell的C層CellHelper, 這是因為Blog的MVC其實又是由多個更小的MVC組成的. M和V沒什麼好說的, 主要說一下作為C的TableVIewHelper做了什麼.
實際開發中, 各個模組的View可能是在Scene對應的Storyboard中新建並佈局的, 此時就不用各個模組自己建立View了(比如這裡的BlogTableViewHelper), 讓Scene傳到C層進行管理就行了, 當然, 如果你是純程式碼的方式, 那View就需要相應模組自行建立了(比如下文的UserInfoViewController), 這個看自己的意願, 無傷大雅.
BlogTableViewHelper對外提供獲取資料和必要的構造方法介面, 內部根據自身情況進行相應的初始化.
當外部呼叫fetchData的介面後, Helper就會啟動獲取資料邏輯, 因為資料獲取前後可能會涉及到一些頁面展示(HUD之類的), 而具體的展示又是和Scene直接相關的(有的Scene展示的是HUD有的可能展示的又是一種樣式或者根本不展示), 所以這部分會以CompletionHandler的形式交由Scene自己處理.
在Helper內部, 資料獲取失敗會展示相應的錯誤頁面, 成功則建立更小的MVC部分並通知其展示資料(也就是通知CellHelper驅動Cell), 另外, TableView的上拉重新整理和下拉載入邏輯也是隸屬於Blog模組的, 所以也在Helper中處理. 在頁面跳轉的邏輯中, 點選跳轉的頁面是由Scene通過VCGeneratorBlock直接配置的, 所以也是解耦的(你也可以通過didSelectRowHandler之類的方式傳遞資料到Scene層, 由Scene做跳轉, 是一樣的).
最後, V(Cell)現在只暴露了Set方法供外部進行設定, 所以和M(Blog)之間也是隔離的, 複用沒有問題.
這一系列過程都是自管理的, 將來如果Blog模組會在另一個SceneX展示, 那麼SceneX只需要新建一個BlogTableViewHelper, 然後呼叫一下helper.fetchData即可.
DraftTableViewHelper和BlogTableViewHelper邏輯類似, 就不貼了, 簡單貼一下UserInfo模組的邏輯:
@implementation UserInfoViewController
+ (instancetype)instanceUserId:(NSUInteger)userId {
return [[UserInfoViewController alloc] initWithUserId:userId];
}
- (instancetype)initWithUserId:(NSUInteger)userId {
// ...略
[self addUI];
// ...略
}
#pragma mark - Action
- (void)onClickIconButton:(UIButton *)sender {
[self.navigationController pushViewController:self.VCGenerator(self.user) animated:YES];
}
#pragma mark - Utils
- (void)addUI {
//各種UI初始化 各種佈局
self.userIconIV = [[UIImageView alloc] initWithFrame:CGRectZero];
self.friendCountLabel = ...
...
}
- (void)fetchData {
[[UserAPIManager new] fetchUserInfoWithUserId:self.userId completionHandler:^(NSError *error, id result) {
if (error) {
[self showErrorInView:self.view info:error.domain];
} else {
self.user = [User objectWithKeyValues:result];
self.userIconIV.image = [UIImage imageWithURL:[NSURL URLWithString:self.user.url]];//資料格式化
self.friendCountLabel.text = [NSString stringWithFormat:@"贊 %ld", self.user.friendCount];//資料格式化
...
}
}];
}
@end
複製程式碼
UserInfoViewController除了比兩個TableViewHelper多個addUI的子控制元件佈局方法, 其他邏輯大同小異, 也是自己管理的MVC, 也是隻需要初始化即可在任何一個Scene中使用.
現在三個自管理模組已經建立完成, UserVC需要的只是根據自己的情況做相應的拼裝佈局即可, 就和搭積木一樣:
@interface UserViewController ()
@property (assign, nonatomic) NSUInteger userId;
@property (strong, nonatomic) UserInfoViewController *userInfoVC;
@property (strong, nonatomic) UITableView *blogTableView;
@property (strong, nonatomic) BlogTableViewHelper *blogTableViewHelper;
@end
@interface SelfViewController : UserViewController
@property (strong, nonatomic) UITableView *draftTableView;
@property (strong, nonatomic) DraftTableViewHelper *draftTableViewHelper;
@end
#pragma mark - UserViewController
@implementation UserViewController
+ (instancetype)instanceWithUserId:(NSUInteger)userId {
if (userId == LoginUserId) {
return [[SelfViewController alloc] initWithUserId:userId];
} else {
return [[UserViewController alloc] initWithUserId:userId];
}
}
- (void)viewDidLoad {
[super viewDidLoad];
[self addUI];
[self configuration];
[self fetchData];
}
#pragma mark - Utils(UserViewController)
- (void)addUI {
//這裡只是表達一下意思 具體的layout邏輯肯定不是這麼簡單的
self.userInfoVC = [UserInfoViewController instanceWithUserId:self.userId];
self.userInfoVC.view.frame = CGRectZero;
[self.view addSubview:self.userInfoVC.view];
[self.view addSubview:self.blogTableView = [[UITableView alloc] initWithFrame:CGRectZero style:0]];
}
- (void)configuration {
self.title = @"使用者詳情";
// ...其他設定
[self.userInfoVC setVCGenerator:^UIViewController *(id params) {
return [UserDetailViewController instanceWithUser:params];
}];
self.blogTableViewHelper = [BlogTableViewHelper helperWithTableView:self.blogTableView userId:self.userId];
[self.blogTableViewHelper setVCGenerator:^UIViewController *(id params) {
return [BlogDetailViewController instanceWithBlog:params];
}];
}
- (void)fetchData {
[self.userInfoVC fetchData];//userInfo模組不需要任何頁面載入提示
[HUD show];//blog模組可能就需要HUD
[self.blogTableViewHelper fetchDataWithcompletionHandler:^(NSError *error, id result) {
[HUD hide];
}];
}
@end
#pragma mark - SelfViewController
@implementation SelfViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self addUI];
[self configuration];
[self fetchData];
}
#pragma mark - Utils(SelfViewController)
- (void)addUI {
[super addUI];
[self.view addSubview:switchButton];//特有部分...
//...各種設定
[self.view addSubview:self.draftTableView = [[UITableView alloc] initWithFrame:CGRectZero style:0]];
}
- (void)configuration {
[super configuration];
self.draftTableViewHelper = [DraftTableViewHelper helperWithTableView:self.draftTableView userId:self.userId];
[self.draftTableViewHelper setVCGenerator:^UIViewController *(id params) {
return [DraftDetailViewController instanceWithDraft:params];
}];
}
- (void)fetchData {
[super fetchData];
[self.draftTableViewHelper fetchData];
}
@end
複製程式碼
作為業務場景的的Scene(UserVC)做的事情很簡單, 根據自身情況對三個模組進行配置(configuration), 佈局(addUI), 然後通知各個模組啟動(fetchData)就可以了, 因為每個模組的展示和互動是自管理的, 所以Scene只需要負責和自身業務強相關的部分即可. 另外, 針對自身訪問的情況我們建立一個UserVC子類SelfVC, SelfVC做的也是類似的事情.
MVC到這就說的差不多了, 對比上面錯誤的MVC方式, 我們看看解決了哪些問題:
1.程式碼複用: 三個小模組的V(cell/userInfoView)對外只暴露Set方法, 對M甚至C都是隔離狀態, 複用完全沒有問題.三個大模組的MVC也可以用於快速構建相似的業務場景(大模組的複用比小模組會差一些, 下文我會說明).
2.程式碼臃腫: 因為Scene大部分的邏輯和佈局都轉移到了相應的MVC中, 我們僅僅是拼裝MVC的便構建了兩個不同的業務場景, 每個業務場景都能正常的進行相應的資料展示, 也有相應的邏輯互動, 而完成這些東西, 加空格也就100行程式碼左右(當然, 這裡我忽略了一下Scene的佈局程式碼).
3.易擴充性: 無論產品未來想加回收站還是防禦塔, 我需要的只是新建相應的MVC模組, 加到對應的Scene即可.
4.可維護性: 各個模組間職責分離, 哪裡出錯改哪裡, 完全不影響其他模組. 另外, 各個模組的程式碼其實並不算多, 哪一天即使寫程式碼的人離職了, 接手的人根據錯誤提示也能快速定位出錯模組.
5.易測試性: 很遺憾, 業務的初始化依然繫結在Scene的生命週期中, 而有些邏輯也仍然需要UI的點選事件觸發, 我們依然只能Command+R, 點點點...
- MVC的缺點
可以看到, 即使是標準的MVC架構也並非完美, 仍然有部分問題難以解決, 那麼MVC的缺點何在? 總結如下: 1.過度的注重隔離: 這個其實MV(x)系列都有這缺點, 為了實現V層的完全隔離, V對外只暴露Set方法, 一般情況下沒什麼問題, 但是當需要設定的屬性很多時, 大量重複的Set方法寫起來還是很累人的.
2.業務邏輯和業務展示強耦合: 可以看到, 有些業務邏輯(頁面跳轉/點贊/分享...)是直接散落在V層的, 這意味著我們在測試這些邏輯時, 必須首先生成對應的V, 然後才能進行測試. 顯然, 這是不合理的. 因為業務邏輯最終改變的是資料M, 我們的關注點應該在M上, 而不是展示M的V.
- MVP
MVC的缺點在於並沒有區分業務邏輯和業務展示, 這對單元測試很不友好. MVP針對以上缺點做了優化, 它將業務邏輯和業務展示也做了一層隔離, 對應的就變成了MVCP. M和V功能不變, 原來的C現在只負責佈局, 而所有的邏輯全都轉移到了P層.
對應關係如圖所示:
業務場景沒有變化, 依然是展示三種資料, 只是三個MVC替換成了三個MVP(圖中我只畫了Blog模組), UserVC負責配置三個MVP(新建各自的VP, 通過VP建立C, C會負責建立VP之間的繫結關係), 並在合適的時機通知各自的P層(之前是通知C層)進行資料獲取, 各個P層在獲取到資料後進行相應處理, 處理完成後會通知繫結的View資料有所更新, V收到更新通知後從P獲取格式化好的資料進行頁面渲染, UserVC最後將已經渲染好的各個View進行佈局即可.
另外, V層C層不再處理任何業務邏輯, 所有事件觸發全部呼叫P層的相應命令, 具體到程式碼中如下:
@interface BlogPresenter : NSObject
+ (instancetype)instanceWithUserId:(NSUInteger)userId;
- (NSArray *)allDatas;//業務邏輯移到了P層 和業務相關的M也跟著到了P層
- (void)refreshUserBlogsWithCompletionHandler:(NetworkTaskCompletionHander)completionHandler;
- (void)loadMoreUserBlogsWithCompletionHandler:(NetworkTaskCompletionHander)completionHandler;
@end
複製程式碼
@interface BlogPresenter()
@property (assign, nonatomic) NSUInteger userId;
@property (strong, nonatomic) NSMutableArray *blogs;
@property (strong, nonatomic) UserAPIManager *apiManager;
@end
@implementation BlogPresenter
+ (instancetype)instanceWithUserId:(NSUInteger)userId {
return [[BlogPresenter alloc] initWithUserId:userId];
}
- (instancetype)initWithUserId:(NSUInteger)userId {
if (self = [super init]) {
self.userId = userId;
self.apiManager = [UserAPIManager new];
//...略
}
}
#pragma mark - Interface
- (NSArray *)allDatas {
return self.blogs;
}
//提供給外層呼叫的命令
- (void)refreshUserBlogsWithCompletionHandler:(NetworkTaskCompletionHander)completionHandler {
[self.apiManager refreshUserBlogsWithUserId:self.userId completionHandler:^(NSError *error, id result) {
if (!error) {
[self.blogs removeAllObjects];//清空之前的資料
for (Blog *blog in result) {
[self.blogs addObject:[BlogCellPresenter presenterWithBlog:blog]];
}
}
completionHandler ? completionHandler(error, result) : nil;
}];
}
//提供給外層呼叫的命令
- (void)loadMoreUserBlogsWithCompletionHandler:(NetworkTaskCompletionHander)completionHandler {
[self.apiManager loadMoreUserBlogsWithUserId:self.userId completionHandler...]
}
@end
複製程式碼
@interface BlogCellPresenter : NSObject
+ (instancetype)presenterWithBlog:(Blog *)blog;
- (NSString *)authorText;
- (NSString *)likeCountText;
- (void)likeBlogWithCompletionHandler:(NetworkTaskCompletionHander)completionHandler;
- (void)shareBlogWithCompletionHandler:(NetworkTaskCompletionHander)completionHandler;
@end
複製程式碼
@implementation BlogCellPresenter
- (NSString *)likeCountText {
return [NSString stringWithFormat:@"贊 %ld", self.blog.likeCount];
}
- (NSString *)authorText {
return [NSString stringWithFormat:@"作者姓名: %@", self.blog.authorName];
}
// ...略
- (void)likeBlogWithCompletionHandler:(NetworkTaskCompletionHander)completionHandler {
[[UserAPIManager new] likeBlogWithBlogId:self.blogId userId:self.userId completionHandler:^(NSError *error, id result) {
if (error) {
//do fail
} else {
//do success
self.blog.likeCount += 1;
}
completionHandler ? completionHandler(error, result) : nil;
}];
}
// ...略
@end
複製程式碼
BlogPresenter和BlogCellPresenter分別作為BlogViewController和BlogCell的P層, 其實就是一系列業務邏輯的集合.
BlogPresenter負責獲取Blogs原始資料並通過這些原始資料構造BlogCellPresenter, 而BlogCellPresenter提供格式化好的各種資料以供Cell渲染, 另外, 點贊和分享的業務現在也轉移到了這裡.
業務邏輯被轉移到了P層, 此時的V層只需要做兩件事:
1.監聽P層的資料更新通知, 重新整理頁面展示.
2.在點選事件觸發時, 呼叫P層的對應方法, 並對方法執行結果進行展示.
@interface BlogCell : UITableViewCell
@property (strong, nonatomic) BlogCellPresenter *presenter;
@end
複製程式碼
@implementation BlogCell
- (void)setPresenter:(BlogCellPresenter *)presenter {
_presenter = presenter;
//從Presenter獲取格式化好的資料進行展示
self.authorLabel.text = presenter.authorText;
self.likeCountLebel.text = presenter.likeCountText;
// ...略
}
#pragma mark - Action
- (void)onClickLikeButton:(UIButton *)sender {
[self.presenter likeBlogWithCompletionHandler:^(NSError *error, id result) {
if (!error) {//頁面重新整理
self.likeCountLebel.text = self.presenter.likeCountText;
}
// ...略
}];
}
@end
複製程式碼
而C層做的事情就是佈局和PV之間的繫結(這裡可能不太明顯, 因為BlogVC裡面的佈局程式碼是TableViewDataSource, PV繫結的話, 因為我偷懶用了Block做通知回撥, 所以也不太明顯, 如果是Protocol回撥就很明顯了), 程式碼如下:
@interface BlogViewController : NSObject
+ (instancetype)instanceWithTableView:(UITableView *)tableView presenter:(BlogPresenter)presenter;
- (void)setDidSelectRowHandler:(void (^)(Blog *))didSelectRowHandler;
- (void)fetchDataWithCompletionHandler:(NetworkCompletionHandler)completionHandler;
@end
複製程式碼
@interface BlogViewController ()<UITableViewDataSource, UITabBarDelegate, BlogView>
@property (weak, nonatomic) UITableView *tableView;
@property (strong, nonatomic) BlogPresenter presenter;
@property (copy, nonatomic) void(^didSelectRowHandler)(Blog *);
@end
@implementation BlogViewController
+ (instancetype)instanceWithTableView:(UITableView *)tableView presenter:(BlogPresenter)presenter {
return [[BlogViewController alloc] initWithTableView:tableView presenter:presenter];
}
- (instancetype)initWithTableView:(UITableView *)tableView presenter:(BlogPresenter)presenter {
if (self = [super init]) {
self.presenter = presenter;
self.tableView = tableView;
tableView.delegate = self;
tableView.dataSource = self;
__weak typeof(self) weakSelf = self;
[tableView registerClass:[BlogCell class] forCellReuseIdentifier:BlogCellReuseIdentifier];
tableView.header = [MJRefreshAnimationHeader headerWithRefreshingBlock:^{//下拉重新整理
[weakSelf.presenter refreshUserBlogsWithCompletionHandler:^(NSError *error, id result) {
[weakSelf.tableView.header endRefresh];
if (!error) {
[weakSelf.tableView reloadData];
}
//...略
}];
}];
tableView.footer = [MJRefreshAnimationFooter headerWithRefreshingBlock:^{//上拉載入
[weakSelf.presenter loadMoreUserBlogsWithCompletionHandler:^(NSError *error, id result) {
[weakSelf.tableView.footer endRefresh];
if (!error) {
[weakSelf.tableView reloadData];
}
//...略
}];
}];
}
return self;
}
#pragma mark - Interface
- (void)fetchDataWithCompletionHandler:(NetworkCompletionHandler)completionHandler {
[self.presenter refreshUserBlogsWithCompletionHandler:^(NSError *error, id result) {
if (error) {
//show error info
} else {
[self.tableView reloadData];
}
completionHandler ? completionHandler(error, result) : nil;
}];
}
#pragma mark - UITableViewDataSource && Delegate
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.presenter.allDatas.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
BlogCell *cell = [tableView dequeueReusableCellWithIdentifier:BlogCellReuseIdentifier];
BlogCellPresenter *cellPresenter = self.presenter.allDatas[indexPath.row];
cell.present = cellPresenter;
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
self.didSelectRowHandler ? self.didSelectRowHandler(self.presenter.allDatas[indexPath.row]) : nil;
}
@end
複製程式碼
BlogViewController現在不再負責實際的資料獲取邏輯, 資料獲取直接呼叫Presenter的相應介面, 另外, 因為業務邏輯也轉移到了Presenter, 所以TableView的佈局用的也是Presenter.allDatas. 至於Cell的展示, 我們替換了原來大量的Set方法, 讓Cell自己根據繫結的CellPresenter做展示. 畢竟現在邏輯都移到了P層, V層要做相應的互動也必須依賴對應的P層命令, 好在V和M仍然是隔離的, 只是和P耦合了, P層是可以隨意替換的, M顯然不行, 這是一種折中.
最後是Scene, 它的變動不大, 只是替換配置MVC為配置MVP, 另外資料獲取也是走P層, 不走C層了(然而程式碼裡面並不是這樣的):
- (void)configuration {
// ...其他設定
BlogPresenter *blogPresenter = [BlogPresenter instanceWithUserId:self.userId];
self.blogViewController = [BlogViewController instanceWithTableView:self.blogTableView presenter:blogPresenter];
[self.blogViewController setDidSelectRowHandler:^(Blog *blog) {
[self.navigationController pushViewController:[BlogDetailViewController instanceWithBlog:blog] animated:YES];
}];
// ...略
}
- (void)fetchData {
// ...略
[self.userInfoVC fetchData];
[HUD show];
[self.blogViewController fetchDataWithCompletionHandler:^(NSError *error, id result) {
[HUD hide];
}];
//還是因為懶, 用了Block走C層轉發會少寫一些程式碼, 如果是Protocol或者KVO方式就會用self.blogViewController.presenter了
//不過沒有關係, 因為我們替換MVC為MVP是為了解決單元測試的問題, 現在的用法完全不影響單元測試, 只是和概念不符罷了.
// ...略
}
複製程式碼
上面的例子中其實有一個問題, 即我們假定: 所有的事件都是由V層主動發起且一次性的. 這其實是不成立的, 舉個簡單的例子: 類似微信語音聊天之類的頁面, 點選語音Cell開始播放, Cell展示播放動畫, 播放完成動畫停止, 然後播放下一條語音.
在這個播放場景中, 如果CellPresenter還是像上面一樣僅僅提供一個playWithCompletionHandler的介面是行不通的. 因為播放完成後回撥肯定是在C層, C層在播放完成後會發現此時執行播放命令的CellPresenter無法通知Cell停止動畫, 即事件的觸發不是一次性的. 另外, 在播放完成後, C層遍歷到下一個待播放CellPresenterX呼叫播放介面時, CellPresenterX因為並不知道它對應的Cell是誰, 當然也就無法通知Cell開始動畫, 即事件的發起者並不一定是V層.
針對這些非一次性或者其他層發起事件, 處理方法其實很簡單, 在CellPresenter加個Block屬性就行了, 因為是屬性, Block可以多次回撥, 另外Block還可以捕獲Cell, 所以也不擔心找不到對應的Cell. 大概這樣:
@interface VoiceCellPresenter : NSObject
@property (copy, nonatomic) void(^didUpdatePlayStateHandler)(NSUInteger);
- (NSURL *)playURL;
@end
複製程式碼
@implementation VoiceCell
- (void)setPresenter:(VoiceCellPresenter *)presenter {
_presenter = presenter;
if (!presenter.didUpdatePlayStateHandler) {
__weak typeof(self) weakSelf = self;
[presenter setDidUpdatePlayStateHandler:^(NSUInteger playState) {
switch (playState) {
case Buffering: weakSelf.playButton... break;
case Playing: weakSelf.playButton... break;
case Paused: weakSelf.playButton... break;
}
}];
}
}
複製程式碼
播放的時候, VC只需要保持一下CellPresenter, 然後傳入相應的playState呼叫didUpdatePlayStateHandler就可以更新Cell的狀態了. 當然, 如果是Protocol的方式進行的VP繫結, 那麼做這些事情就很平常了, 就不寫了.
MVP大概就是這個樣子了, 相對於MVC, 它其實只做了一件事情, 即分割業務展示和業務邏輯. 展示和邏輯分開後, 只要我們能保證V在收到P的資料更新通知後能正常重新整理頁面, 那麼整個業務就沒有問題. 因為V收到的通知其實都是來自於P層的資料獲取/更新操作, 所以我們只要保證P層的這些操作都是正常的就可以了. 即我們只用測試P層的邏輯, 不必關心V層的情況.
- MVVM
MVP其實已經是一個很好的架構, 幾乎解決了所有已知的問題, 那麼為什麼還會有MVVM呢? 仍然是舉例說明, 假設現在有一個Cell, 點選Cell上面的關注按鈕可以是加關注, 也可以是取消關注, 在取消關注時, SceneA要求先彈窗詢問, 而SceneB則不做彈窗, 那麼此時的取消關注操作就和業務場景強關聯, 所以這個介面不可能是V層直接呼叫, 會上升到Scene層.具體到程式碼中, 大概這個樣子:
@interface UserCellPresenter : NSObject
@property (copy, nonatomic) void(^followStateHander)(BOOL isFollowing);
@property (assign, nonatomic) BOOL isFollowing;
- (void)follow;
@end
複製程式碼
@implementation UserCellPresenter
- (void)follow {
if (!self.isFollowing) {//未關注 去關注
// follow user
} else {//已關注 則取消關注
self.followStateHander ? self.followStateHander(YES) : nil;//先通知Cell顯示follow狀態
[[FollowAPIManager new] unfollowWithUserId:self.userId completionHandler:^(NSError *error, id result) {
if (error) {
self.followStateHander ? self.followStateHander(NO) : nil;//follow失敗 狀態回退
} eles {
self.isFollowing = YES;
}
//...略
}];
}
}
@end
複製程式碼
@implementation UserCell
- (void)setPresenter:(UserCellPresenter *)presenter {
_presenter = presenter;
if (!_presenter.followStateHander) {
__weak typeof(self) weakSelf = self;
[_presenter setFollowStateHander:^(BOOL isFollowing) {
[weakSelf.followStateButton setImage:isFollowing ? : ...];
}];
}
}
- (void)onClickFollowButton:(UIButton *)button {//將關注按鈕點選事件上傳
[self routeEvent:@"followEvent" userInfo:@{@"presenter" : self.presenter}];
}
@end
複製程式碼
@implementation FollowListViewController
//攔截點選事件 判斷後確認是否執行事件
- (void)routeEvent:(NSString *)eventName userInfo:(NSDictionary *)userInfo {
if ([eventName isEqualToString:@"followEvent"]) {
UserCellPresenter *presenter = userInfo[@"presenter"];
[self showAlertWithTitle:@"提示" message:@"確認取消對他的關注嗎?" cancelHandler:nil confirmHandler: ^{
[presenter follow];
}];
}
}
@end
複製程式碼
@implementation UIResponder (Router)
//沿著響應者鏈將事件上傳 事件最終被攔截處理 或者 無人處理直接丟棄
- (void)routeEvent:(NSString *)eventName userInfo:(NSDictionary *)userInfo {
[self.nextResponder routeEvent:eventName userInfo:userInfo];
}
@end
複製程式碼
Block方式看起來略顯繁瑣, 我們換到Protocol看看:
@protocol UserCellPresenterCallBack <NSObject>
- (void)userCellPresenterDidUpdateFollowState:(BOOL)isFollowing;
@end
@interface UserCellPresenter : NSObject
@property (weak, nonatomic) id<UserCellPresenterCallBack> view;
@property (assign, nonatomic) BOOL isFollowing;
- (void)follow;
@end
複製程式碼
@implementation UserCellPresenter
- (void)follow {
if (!self.isFollowing) {//未關注 去關注
// follow user
} else {//已關注 則取消關注
BOOL isResponse = [self.view respondsToSelector:@selector(userCellPresenterDidUpdateFollowState)];
isResponse ? [self.view userCellPresenterDidUpdateFollowState:YES] : nil;
[[FollowAPIManager new] unfollowWithUserId:self.userId completionHandler:^(NSError *error, id result) {
if (error) {
isResponse ? [self.view userCellPresenterDidUpdateFollowState:NO] : nil;
} eles {
self.isFollowing = YES;
}
//...略
}];
}
}
@end
複製程式碼
@implementation UserCell
- (void)setPresenter:(UserCellPresenter *)presenter {
_presenter = presenter;
_presenter.view = self;
}
#pragma mark - UserCellPresenterCallBack
- (void)userCellPresenterDidUpdateFollowState:(BOOL)isFollowing {
[self.followStateButton setImage:isFollowing ? : ...];
}
複製程式碼
除去Route和VC中Alert之類的程式碼, 可以發現無論是Block方式還是Protocol方式因為需要對頁面展示和業務邏輯進行隔離, 程式碼上饒了一小圈, 無形中增添了不少的程式碼量, 這裡僅僅只是一個事件就這樣, 如果是多個呢? 那寫起來真是蠻傷的...
仔細看一下上面的程式碼就會發現, 如果我們繼續新增事件, 那麼大部分的程式碼都是在做一件事情: P層將資料更新通知到V層.
Block方式會在P層新增很多屬性, 在V層新增很多設定Block邏輯. 而Protocol方式雖然P層只新增了一個屬性, 但是Protocol裡面的方法卻會一直增加, 對應的V層也就需要增加的方法實現.
問題既然找到了, 那就試著去解決一下吧, OC中能夠實現兩個物件間的低耦合通訊, 除了Block和Protocol, 一般都會想到KVO. 我們看看KVO在上面的例子有何表現:
@interface UserCellViewModel : NSObject
@property (assign, nonatomic) BOOL isFollowing;
- (void)follow;
@end
複製程式碼
@implementation UserCellViewModel
- (void)follow {
if (!self.isFollowing) {//未關注 去關注
// follow user
} else {//已關注 則取消關注
self.isFollowing = YES;//先通知Cell顯示follow狀態
[[FollowAPIManager new] unfollowWithUserId:self.userId completionHandler:^(NSError *error, id result) {
if (error) { self.isFollowing = NO; }//follow失敗 狀態回退
//...略
}];
}
}
@end
複製程式碼
@implementation UserCell
- (void)awakeFromNib {
@weakify(self);
[RACObserve(self, viewModel.isFollowing) subscribeNext:^(NSNumber *isFollowing) {
@strongify(self);
[self.followStateButton setImage:[isFollowing boolValue] ? : ...];
};
}
複製程式碼
程式碼大概少了一半左右, 另外, 邏輯讀起來也清晰多了, Cell觀察繫結的ViewModel的isFollowing狀態, 並在狀態改變時, 更新自己的展示. 三種資料通知方式簡單一比對, 相信哪種方式對程式設計師更加友好, 大家都心裡有數, 就不做贅述了.
現在大概一提到MVVM就會想到RAC, 但這兩者其實並沒有什麼聯絡, 對於MVVM而言RAC只是提供了優雅安全的資料繫結方式, 如果不想學RAC, 自己搞個KVOHelper之類的東西也是可以的. 另外 ,RAC的魅力其實在於函式式響應式程式設計, 我們不應該僅僅將它侷限於MVVM的應用, 日常的開發中也應該多使用使用的.
關於MVVM, 我想說的就是這麼多了, 因為MVVM其實只是MVP的繫結進化體, 除去資料繫結方式, 其他的和MVP如出一轍, 只是可能呈現方式是Command/Signal而不是CompletionHandler之類的, 故不做贅述.
最後做個簡單的總結吧:
1.MVC作為老牌架構, 優點在於將業務場景按展示資料型別劃分出多個模組, 每個模組中的C層負責業務邏輯和業務展示, 而M和V應該是互相隔離的以做重用, 另外每個模組處理得當也可以作為重用單元. 拆分在於解耦, 順便做了減負, 隔離在於重用, 提升開發效率. 缺點是沒有區分業務邏輯和業務展示, 對單元測試不友好.
2.MVP作為MVC的進階版, 提出區分業務邏輯和業務展示, 將所有的業務邏輯轉移到P層, V層接受P層的資料更新通知進行頁面展示. 優點在於良好的分層帶來了友好的單元測試, 缺點在於分層會讓程式碼邏輯優點繞, 同時也帶來了大量的程式碼工作, 對程式設計師不夠友好.
3.MVVM作為集大成者, 通過資料繫結做資料更新, 減少了大量的程式碼工作, 同時優化了程式碼邏輯, 只是學習成本有點高, 對新手不夠友好.
4.MVP和MVVM因為分層所以會建立MVC兩倍以上的檔案類, 需要良好的程式碼管理方式.
5.在MVP和MVVM中, V和P或者VM之間理論上是多對多的關係, 不同的佈局在相同的邏輯下只需要替換V層, 而相同的佈局不同的邏輯只需要替換P或者VM層. 但實際開發中P或者VM往往因為耦合了V層的展示邏輯退化成了一對一關係(比如SceneA中需要顯示"xxx+Name", VM就將Name格式化為"xxx + Name". 某一天SceneB也用到這個模組, 所有的點選事件和頁面展示都一樣, 只是Name展示為"yyy + Name", 此時的VM因為耦合SceneA的展示邏輯, 就顯得比較尷尬), 針對此類情況, 通常有兩種辦法, 一種是在VM層加狀態進而判斷輸出狀態, 一種是在VM層外再加一層FormatHelper. 前者可能因為狀態過多顯得程式碼難看, 後者雖然比較優雅且擴充性高, 但是過多的分層在資料還原時就略顯笨拙, 大家應該按需選擇.
這裡隨便瞎扯一句, 有些文章上來就說MVVM是為了解決C層臃腫, MVC難以測試的問題, 其實並不是這樣的. 按照架構演進順序來看, C層臃腫大部分是沒有拆分好MVC模組, 好好拆分就行了, 用不著MVVM. 而MVC難以測試也可以用MVP來解決, 只是MVP也並非完美, 在VP之間的資料互動太繁瑣, 所以才引出了MVVM. 當MVVM這個完全體出現以後, 我們從結果看起源, 發現它做了好多事情, 其實並不是, 它的前輩們付出的努力也並不少!
- 架構那麼多, 日常開發中到底該如何選擇?
不管是MVC, MVP, MVVM還是MVXXX, 最終的目的在於服務於人, 我們注重架構, 注重分層都是為了開發效率, 說到底還是為了開心. 所以, 在實際開發中不應該拘泥於某一種架構, 根據實際專案出發, 一般普通的MVC就能應對大部分的開發需求, 至於MVP和MVVM, 可以嘗試, 但不要強制. 總之, 希望大家能做到: 設計時, 心中有數. 擼碼時, 開心就好.
=======================分割線=======================
這篇部落格放出來以後, 陸陸續續收到一些小夥伴的簡信, 大部分都是一些提問, 但是每個人的問題都比較雷同, 趁著清明放假有時間, 這裡將問得比較頻繁的問題擇出來, 這樣以後類似的問題就不用一一回復了, 算是解個懶.
Q: 為什麼有的時候是MVP/MVVM有的時候是MVCP/MVCVM.
A: 其實這個問題在demo的MVP部分有做解釋, 估計有些朋友沒有看demo, 或者是我描述得太含糊了沒能讓人明白. 那麼這裡我分別給出MVVM和MVCVM的例子, 結合程式碼解釋會方便一些, 順便也回答一下UserInfo模組用MVVM怎麼寫.
@interface UserInfoViewModel : NSObject
+ (instancetype)viewModelWithUserId:(NSUInteger)userId;
- (User *)user;
- (RACCommand *)fetchUserInfoCommand;
- (UIImage *)icon;
- (NSString *)name;
- (NSString *)summary;
- (NSString *)blogCount;
- (NSString *)friendCount;
@end
複製程式碼
@interface UserInfoViewModel ()
@property (strong, nonatomic) UIImage *icon;
@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *summary;
@property (copy, nonatomic) NSString *blogCount;
@property (copy, nonatomic) NSString *friendCount;
@property (strong, nonatomic) User *user;
@property (assign, nonatomic) NSUInteger userId;
@end
@implementation UserInfoViewModel
+ (instancetype)viewModelWithUserId:(NSUInteger)userId {
UserInfoViewModel *viewModel = [UserInfoViewModel new];
viewModel.userId = userId;
return viewModel;
}
- (RACCommand *)fetchUserInfoCommand {
return [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [[self fetchUserInfoSignal] doNext:^(User *user) {
self.user = user;
self.icon = [UIImage imageNamed:user.icon ?: @"icon0"];
self.name = user.name.length > 0 ? user.name : @"匿名";
self.summary = [NSString stringWithFormat:@"個人簡介: %@", user.summary.length > 0 ? user.summary : @"這個人很懶, 什麼也沒有寫~"];
self.blogCount = [NSString stringWithFormat:@"作品: %ld", user.blogCount];
self.friendCount = [NSString stringWithFormat:@"好友: %ld", user.friendCount];
}];
}];
}
- (RACSignal *)fetchUserInfoSignal {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[[UserAPIManager new] fetchUserInfoWithUserId:self.user.userId completionHandler:^(NSError *error, id result) {
if (!error) {
[subscriber sendNext:result];
[subscriber sendCompleted];
} else {
[subscriber sendError:error];
}
}];
return nil;
}];
}
複製程式碼
UserInfoViewModel做的事情很簡單, 從伺服器拉取資料, 然後將資料格式化為V層需要展示的樣子, 這部分MVVM和MVCVM都是一樣的, 接下來我們看看不一樣的部分, 先看看MVVM中的V層程式碼:
#import "UserInfoViewModel.h"
@interface UserInfoView : UIView
+ (instancetype)instanceWithViewModel:(UserInfoViewModel *)viewModel;
- (void)fetchData;
- (void)setOnClickIconCommand:(RACCommand *)onClickIconCommand;
@end
複製程式碼
@interface UserInfoView ()
@property (weak, nonatomic) UIButton *iconButton;
@property (weak, nonatomic) UILabel *nameLabel;
@property (weak, nonatomic) UILabel *summaryLabel;
@property (weak, nonatomic) UILabel *blogCountLabel;
@property (weak, nonatomic) UILabel *friendCountLabel;
@property (strong, nonatomic) RACCommand *onClickIconCommand;
@property (strong, nonatomic) UserInfoViewModel *viewModel;
@end
@implementation UserInfoView
+ (instancetype)instanceWithViewModel:(UserInfoViewModel *)viewModel {
UserInfoView *view = [UserInfoView new];
view.viewModel = viewModel;
return view;
}
- (instancetype)init {
return [self initWithFrame:CGRectZero];
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self addUI];
[self bind];
}
return self;
}
- (void)bind {
RAC(self.nameLabel, text) = RACObserve(self, viewModel.name);
RAC(self.summaryLabel, text) = RACObserve(self, viewModel.summary);
RAC(self.blogCountLabel, text) = RACObserve(self, viewModel.blogCount);
RAC(self.friendCountLabel, text) = RACObserve(self, viewModel.friendCount);
@weakify(self);
[RACObserve(self, viewModel.icon) subscribeNext:^(UIImage *icon) {
@strongify(self);
[self.iconButton setImage:icon forState:UIControlStateNormal];
}];
[[self.iconButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
@strongify(self);
[self.onClickIconCommand execute:self.viewModel.user];
}];
}
- (void)fetchData {
[[[self.viewModel fetchUserInfoCommand] execute:nil] subscribeError:^(NSError *error) {
//show error view
} completed:^{
//do completed
}];
}
- (void)addUI {
//... 各種新建 各種佈局
}
@end
複製程式碼
然後再看看MVCVM中V層程式碼:
@interface UserInfoView : UIView
- (UIButton *)iconButton;
- (UILabel *)nameLabel;
- (UILabel *)summaryLabel;
- (UILabel *)blogCountLabel;
- (UILabel *)friendCountLabel;
@end
複製程式碼
@interface UserInfoView ()
@property (weak, nonatomic) UIButton *iconButton;
@property (weak, nonatomic) UILabel *nameLabel;
@property (weak, nonatomic) UILabel *summaryLabel;
@property (weak, nonatomic) UILabel *blogCountLabel;
@property (weak, nonatomic) UILabel *friendCountLabel;
@end
@implementation UserInfoView
- (instancetype)init {
return [self initWithFrame:CGRectZero];
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self addUI];
}
return self;
}
- (void)addUI {
//... 各種新建 各種佈局
}
@end
複製程式碼
在MVVM中的UserInfoView一共做了三件事情: 1. UI佈局(addUI), 2. 資料繫結(bind) 3. 和上層互動(fetchData, onClickIconCommand) 相對而言, MVCVM中的UserInfoView做的事情就少多了, 只做了一件事情: UI佈局. 不過它不僅布了局, 還將對應的View也暴露了出來. 這些暴露出來的東西給誰用呢? 還有, 資料繫結和上層互動現在由誰來做呢? 顯然只能是這個多出來的C層了, 看看這部分的程式碼吧:
@interface UserInfoController : NSObject
+ (instancetype)instanceWithView:(UserInfoView *)view viewModel:(UserInfoViewModel *)viewModel;
- (UserInfoView *)view;
- (void)fetchData;
- (void)setOnClickIconCommand:(RACCommand *)onClickIconCommand;
@end
複製程式碼
@interface UserInfoController ()
@property (strong, nonatomic) UserInfoView *view;
@property (strong, nonatomic) UserInfoViewModel *viewModel;
@property (strong, nonatomic) RACCommand *onClickIconCommand;
@end
@implementation UserInfoController
+ (instancetype)instanceWithView:(UserInfoView *)view viewModel:(UserInfoViewModel *)viewModel {
if (view == nil || viewModel == nil) { return nil; }
return [[UserInfoController alloc] initWithView:view viewModel:viewModel];
}
- (instancetype)initWithView:(UserInfoView *)view viewModel:(UserInfoViewModel *)viewModel {
if (self = [super init]) {
self.view = view;
self.viewModel = viewModel;
[self bind];
}
return self;
}
- (void)bind {
RAC(self.view.nameLabel, text) = RACObserve(self, viewModel.name);
RAC(self.view.summaryLabel, text) = RACObserve(self, viewModel.summary);
RAC(self.view.blogCountLabel, text) = RACObserve(self, viewModel.blogCount);
RAC(self.view.friendCountLabel, text) = RACObserve(self, viewModel.friendCount);
@weakify(self);
[RACObserve(self, viewModel.icon) subscribeNext:^(UIImage *icon) {
@strongify(self);
[self.view.iconButton setImage:icon forState:UIControlStateNormal];
}];
[[self.view.iconButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
@strongify(self);
[self.onClickIconCommand execute:self.viewModel.user];
}];
}
- (void)fetchData {
[[[self.viewModel fetchUserInfoCommand] execute:nil] subscribeError:^(NSError *error) {
//show error view
} completed:^{
//do completed
}];
}
@end
複製程式碼
程式碼一亮出來, 相信各位應該很清楚MVP/MVVM和MVCP/MVCVM的區別何在了, 簡單描述一下就是是否拆分UI佈局和資料繫結(注意: 是資料繫結, 不是業務邏輯, 業務邏輯都在VM層).
毫無疑問, 拆分更加細緻的MVCVM比MVVM要好一些, 純佈局的V層優點在MVC部分已經介紹過了, 複用性賊好, 另外, 佈局拆出來以後, 資料繫結層的程式碼看起來會更加簡潔, 易讀性也很好. 然而, 最初的demo裡面並沒有包含這種寫法的例子, 這算是我自己的原因. 因為實際開發通常沒有這麼細粒度的複用模組(UI和產品不給機會), 另外我本人習慣用xib/sb做頁面佈局, 所以V層也不會有什麼佈局程式碼, 久而久之, 自己寫的程式碼都是MVVM而不是MVCVM, 習慣成自然了.
Q: V層直接宣告瞭P/VM的屬性, 資料繫結又是寫死的, 那不就是一對一了, 怎麼複用呢?
A: 注意到我描述P/VM層時都是說: xxxP/VM.h暴露了那些介面, 而不是xxxP/VM有那些屬性. 換句話說, P/VM其實只是定義了一套規範, 但是這套規範的實現卻是千差萬別的, 當只有一個實現時確實是一對一的, 當有多個實現時就是一對多了. 舉個我專案中的例子吧, 我有好友列表, 關注列表, 使用者列表三個不同資料來源不同資料操作的列表, 但這三張表cell的佈局展示卻是一模一樣的, 只是展示的文字不一樣, 點選按鈕有的是加/取消好友, 有的是加/取消關注, 這就是典型的佈局不變但是邏輯變化的例子, 所以我只寫了一個cell, 一個cellViewModel介面, 但是viewModel的介面實現卻是兩套, 對應到程式碼中:
//HHUserCellViewModel.h
@interface HHUserCellViewModel : NSObject
+ (instancetype)friendCellViewModelWithUser:(HHUser *)user;
+ (instancetype)followCellViewModelWithUser:(HHFriend *)user;
- (id)user;
- (BOOL)isVip;
- (NSURL *)userAvatarURL;
- (NSString *)userName;
- (NSString *)userSignature;
- (NSString *)userFriendCount;
- (NSString *)rightButtonTitle;
- (NSString *)rightButtonEventName;
- (RACCommand *)rightButtonCommand;
- (BOOL)deleteButtonHidden;
- (UIImage *)deleteButtonImage;
- (RACCommand *)deleteButtonCommand;
- (CGFloat)contentHeight;
@end
複製程式碼
//HHUserCellViewModel基類: 這裡定義了兩套實現都會用到的屬性和方法
@interface HHUserCellViewModel ()
@property (strong, nonatomic) HHUser *user;
@property (copy, nonatomic) NSString *rightButtonTitle;
@property (strong, nonatomic) RACCommand *rightButtonCommand;
@property (assign, nonatomic) BOOL deleteButtonHidden;
@property (strong, nonatomic) UIImage *deleteButtonImage;
@property (strong, nonatomic) RACCommand *deleteButtonCommand;
@end
#pragma mark - HHFollowCellViewModel
//HHFollowCellViewModel子類: 關注模式的viewModel的實現
@interface HHFollowCellViewModel : HHUserCellViewModel
@end
@implementation HHFollowCellViewModel
- (instancetype)initWithUser:(HHFriend *)user {
if (self = [super initWithUser:user]) {
[self switchRightButtonColor:user.followState];
@weakify(self);
self.rightButtonCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
@strongify(self);
if ([self.rightButtonTitle isEqualToString:@"已關注"]) {
self.deleteButtonHidden = !self.deleteButtonHidden;
return [RACSignal empty];
} else {
[self switchRightButtonColor:YES];
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
//點選右側按鈕呼叫加關注介面
[[HHSocketFollowAPIManager new] followWithFollowUser:self.user completionHandler:^(NSError *error, id result) {
if (error) {
[self switchRightButtonColor:NO];
}
if ([USER_ID integerValue] != 0) {
[subscriber sendNext:@(error == nil)];
}
[subscriber sendCompleted];
}];
return nil;
}];
}
}];
self.deleteButtonImage = [UIImage imageNamed:@"unfollow.png"];
self.deleteButtonCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
//點選刪除按鈕呼叫取消關注介面
self.deleteButtonHidden = YES;
[[HHSocketFollowAPIManager new] unfollowWithUnfollowUser:self.user completionHandler:^(NSError *error, id result) {
if (error) {
[subscriber sendError:error];
} else {
[self switchRightButtonColor:NO];
[subscriber sendCompleted];
}
}];
return nil;
}];
}];
}
return self;
}
- (void)switchRightButtonColor:(BOOL)isSelected {
[super switchRightButtonColor:isSelected];
self.rightButtonTitle = isSelected ? @"已關注" : @"+關注";
}
- (CGFloat)contentHeight {
return 68;
}
@end
#pragma mark - HHFriendCellViewModel
//HHFriendCellViewModel子類: 好友模式的viewModel的實現
@interface HHFriendCellViewModel : HHUserCellViewModel
@end
@implementation HHFriendCellViewModel
- (instancetype)initWithUser:(HHFriend *)user {
if (self = [super initWithUser:user]) {
[self switchRightButtonColorWithFriendState:self.user.friendState];
@weakify(self);
self.rightButtonCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
@strongify(self);
if ([self.rightButtonTitle isEqualToString:@"加好友"]) {
[self switchRightButtonColorWithFriendState:1];
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
//點選右側按鈕呼叫加好友介面
[[HHSocketFriendAPIManager new] addFriendWithUser:self.user msg:@"你好, 我是xxx" completionHandler:^(NSError *error, id result) {
if (error) {
[self switchRightButtonColorWithFriendState:0];
}
if ([USER_ID integerValue] != 0) {
[subscriber sendNext:@(error == nil)];
}
[subscriber sendCompleted];
}];
return nil;
}];
} else if([self.rightButtonTitle isEqualToString:@"好友"]) {
self.deleteButtonHidden = !self.deleteButtonHidden;
}
return [RACSignal empty];
}];
self.deleteButtonImage = [UIImage imageNamed:@"deleteFriend.png"];
self.deleteButtonCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
self.deleteButtonHidden = !self.deleteButtonHidden;
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
//點選刪除按鈕呼叫刪除好友介面
[[HHSocketFriendAPIManager new] deleteFriendWithUser:self.user completionHandler:^(NSError *error, id result) {
if (error) {
[subscriber sendError:error];
} else {
[self switchRightButtonColorWithFriendState:0];
[subscriber sendCompleted];
}
}];
return nil;
}];
}];
}
return self;
}
- (void)switchRightButtonColorWithFriendState:(NSInteger)state {
self.user.friendState = state;
switch (state) {
case 0: {
[super switchRightButtonColor:NO];
self.rightButtonTitle = @"加好友";
} break;
case 1: {
self.rightButtonTitleColor = kColorGrayNine;
self.rightButtonBorderColor = self.rightButtonBackgroundColor = [UIColor whiteColor];
self.rightButtonTitle = @"驗證中";
} break;
case 2: {
[super switchRightButtonColor:YES];
self.rightButtonTitle = @"好友";
} break;
}
}
- (CGFloat)contentHeight {
return self.user.userId != [USER_ID integerValue] && self.user.commonFriendCount > 0 ? 91 : 68;
}
@end
#pragma mark - HHUserCellViewModel
@implementation HHUserCellViewModel
+ (instancetype)friendCellViewModelWithUser:(HHFriend *)user {
return [[HHFriendCellViewModel alloc] initWithUser:user];
}
+ (instancetype)followCellViewModelWithUser:(HHFriend *)user {
return [[HHFollowCellViewModel alloc] initWithUser:user];
}
//HHUserCellViewModel基類: 一些實現相同的介面直接在此處實現 免得重複一模一樣的程式碼
#pragma mark - PublicInterface
- (BOOL)isVip {
return self.user.level > 0;
}
- (NSString *)userName {
return self.user.nickname;
}
- (NSString *)userFriendCount {
return self.user.commonFriendCount > 0 ? [NSString stringWithFormat:@"你們有%ld個共同好友", self.user.commonFriendCount] : @"";
}
- (NSString *)userSignature {
return self.user.signature.length > 0 ? self.user.signature : @"TA很懶,什麼都沒寫";
}
- (NSURL *)userAvatarURL {
return self.user.avatar.HHUrl;
}
複製程式碼
對於Cell而言, 它只知道自己該怎麼樣佈局, 自己會有一個實現了HHUserCellViewModel介面的屬性, 然後會去繫結這些介面的資料進行展示, 點選以後呼叫哪個Command, 至於具體展示出來的是好友還是關注, 點選具體會執行什麼事件, 它完全不關心, 它只管繫結, 其他的事情上層會處理好的.
這裡也是出於個人習慣, 我本人特別喜歡用類簇或者說抽象工廠, 因為這樣能少建很多檔案, 一個類就能做完所有事情. 現在想來, 如果一開始demo裡面寫的就是Protocol, 然後多個類實現這個Protocol可能就不會有人有疑問了.
額... 本來有好幾個問題的, 但是簡書有字數限制(程式碼好像也算字?), 我又不太想另開一遍囉嗦囉嗦, 只能選擇把相對重要的MVVM這部分放出來了, 望海涵