iOS 之MVC架構(gòu)

一驻子、MVC的相關(guān)概念
MVC最早存在于桌面程序中的, M是指業(yè)務(wù)數(shù)據(jù), V是指用戶界面, C則是控制器. 在具體的業(yè)務(wù)場(chǎng)景中, C作為M和V之間的連接, 負(fù)責(zé)獲取輸入的業(yè)務(wù)數(shù)據(jù), 然后將處理后的數(shù)據(jù)輸出到界面上做相應(yīng)展示, 另外, 在數(shù)據(jù)有所更新時(shí), C還需要及時(shí)提交相應(yīng)更新到界面展示. 在上述過(guò)程中, 因?yàn)镸和V之間是完全隔離的, 所以在業(yè)務(wù)場(chǎng)景切換時(shí), 通常只需要替換相應(yīng)的C, 復(fù)用已有的M和V便可快速搭建新的業(yè)務(wù)場(chǎng)景. MVC因其復(fù)用性, 大大提高了開(kāi)發(fā)效率, 現(xiàn)已被廣泛應(yīng)用在各端開(kāi)發(fā)中.

1租漂、MVC之消失的C層

業(yè)務(wù)場(chǎng)景

上圖中的頁(yè)面(業(yè)務(wù)場(chǎng)景)或者類似頁(yè)面相信大家做過(guò)不少, 各個(gè)程序員的具體實(shí)現(xiàn)方式可能各不一樣, 這里說(shuō)說(shuō)我所看到的部分程序員的寫法:

//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一跑, 沒(méi)有問(wèn)題, 心滿意足的做其他事情去了. 后來(lái)有一天, 產(chǎn)品要求這個(gè)業(yè)務(wù)需要改動(dòng), 用戶在看他人信息時(shí)是上圖中的頁(yè)面, 看自己的信息時(shí), 多一個(gè)草稿箱的展示, 像這樣:


新業(yè)務(wù)場(chǎng)景

于是小白將代碼改成這樣:

//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 {
    ...同上
}

后來(lái)啊, 產(chǎn)品覺(jué)得用戶看自己的頁(yè)面再加個(gè)回收站什么的會(huì)很好, 于是程序員又加上一段代碼邏輯 , 再后來(lái)...
隨著需求的變更, UserVC變得越來(lái)越臃腫, 越來(lái)越難以維護(hù), 拓展性和測(cè)試性也極差. 程序員也發(fā)現(xiàn)好像代碼寫得有些問(wèn)題, 但是問(wèn)題具體出在哪里? 難道這不是MVC嗎?
我們將上面的過(guò)程用一張圖來(lái)表示:


錯(cuò)誤MVC

通過(guò)這張圖可以發(fā)現(xiàn), 用戶信息頁(yè)面作為業(yè)務(wù)場(chǎng)景Scene需要展示多種數(shù)據(jù)M(Blog/Draft/UserInfo), 所以對(duì)應(yīng)的有多個(gè)View(blogTableView/draftTableView/image...), 但是, 每個(gè)MV之間并沒(méi)有一個(gè)連接層C, 本來(lái)應(yīng)該分散到各個(gè)C層處理的邏輯全部被打包丟到了Scene這一個(gè)地方處理, 也就是M-C-V變成了MM...-Scene-...VV, C層就這樣莫名其妙的消失了.
另外, 作為V的兩個(gè)cell直接耦合了M(blog/draft), 這意味著這兩個(gè)V的輸入被綁死到了相應(yīng)的M上, 復(fù)用無(wú)從談起.
最后, 針對(duì)這個(gè)業(yè)務(wù)場(chǎng)景的測(cè)試異常麻煩, 因?yàn)闃I(yè)務(wù)初始化和銷毀被綁定到了VC的生命周期上, 而相應(yīng)的邏輯也關(guān)聯(lián)到了和View的點(diǎn)擊事件, 測(cè)試只能Command+R, 點(diǎn)點(diǎn)點(diǎn)...

2艰争、正確的MVC使用姿勢(shì)

也許是UIViewController的類名給新人帶來(lái)了迷惑, 讓人誤以為VC就一定是MVC中的C層, 又或許是Button, Label之類的View太過(guò)簡(jiǎn)單完全不需要一個(gè)C層來(lái)配合, 總之, 我工作以來(lái)經(jīng)歷的項(xiàng)目中見(jiàn)過(guò)太多這樣的"MVC". 那么, 什么才是正確的MVC使用姿勢(shì)呢?
仍以上面的業(yè)務(wù)場(chǎng)景舉例, 正確的MVC應(yīng)該是這個(gè)樣子的:


正確MVC

UserVC作為業(yè)務(wù)場(chǎng)景, 需要展示三種數(shù)據(jù), 對(duì)應(yīng)的就有三個(gè)MVC, 這三個(gè)MVC負(fù)責(zé)各自模塊的數(shù)據(jù)獲取, 數(shù)據(jù)處理和數(shù)據(jù)展示, 而UserVC需要做的就是配置好這三個(gè)MVC, 并在合適的時(shí)機(jī)通知各自的C層進(jìn)行數(shù)據(jù)獲取, 各個(gè)C層拿到數(shù)據(jù)后進(jìn)行相應(yīng)處理, 處理完成后渲染到各自的View上, UserVC最后將已經(jīng)渲染好的各個(gè)View進(jìn)行布局即可, 具體到代碼中如下:

@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;
    //...各種設(shè)置
    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)構(gòu)成, 這里有點(diǎn)特殊, blogs里面裝的不是M, 而是Cell的C層CellHelper, 這是因?yàn)锽log的MVC其實(shí)又是由多個(gè)更小的MVC組成的. M和V沒(méi)什么好說(shuō)的, 主要說(shuō)一下作為C的TableVIewHelper做了什么.

實(shí)際開(kāi)發(fā)中, 各個(gè)模塊的View可能是在Scene對(duì)應(yīng)的Storyboard中新建并布局的, 此時(shí)就不用各個(gè)模塊自己建立View了(比如這里的BlogTableViewHelper), 讓Scene傳到C層進(jìn)行管理就行了, 當(dāng)然, 如果你是純代碼的方式, 那View就需要相應(yīng)模塊自行建立了(比如下文的UserInfoViewController), 這個(gè)看自己的意愿, 無(wú)傷大雅.

BlogTableViewHelper對(duì)外提供獲取數(shù)據(jù)和必要的構(gòu)造方法接口, 內(nèi)部根據(jù)自身情況進(jìn)行相應(yīng)的初始化.
當(dāng)外部調(diào)用fetchData的接口后, Helper就會(huì)啟動(dòng)獲取數(shù)據(jù)邏輯, 因?yàn)閿?shù)據(jù)獲取前后可能會(huì)涉及到一些頁(yè)面展示(HUD之類的), 而具體的展示又是和Scene直接相關(guān)的(有的Scene展示的是HUD有的可能展示的又是一種樣式或者根本不展示), 所以這部分會(huì)以CompletionHandler的形式交由Scene自己處理.
在Helper內(nèi)部, 數(shù)據(jù)獲取失敗會(huì)展示相應(yīng)的錯(cuò)誤頁(yè)面, 成功則建立更小的MVC部分并通知其展示數(shù)據(jù)(也就是通知CellHelper驅(qū)動(dòng)Cell), 另外, TableView的上拉刷新和下拉加載邏輯也是隸屬于Blog模塊的, 所以也在Helper中處理.
在頁(yè)面跳轉(zhuǎn)的邏輯中, 點(diǎn)擊跳轉(zhuǎn)的頁(yè)面是由Scene通過(guò)VCGeneratorBlock直接配置的, 所以也是解耦的(你也可以通過(guò)didSelectRowHandler之類的方式傳遞數(shù)據(jù)到Scene層, 由Scene做跳轉(zhuǎn), 是一樣的).
最后, V(Cell)現(xiàn)在只暴露了Set方法供外部進(jìn)行設(shè)置, 所以和M(Blog)之間也是隔離的, 復(fù)用沒(méi)有問(wèn)題.

這一系列過(guò)程都是自管理的, 將來(lái)如果Blog模塊會(huì)在另一個(gè)SceneX展示, 那么SceneX只需要新建一個(gè)BlogTableViewHelper, 然后調(diào)用一下helper.fetchData即可.

DraftTableViewHelper和BlogTableViewHelper邏輯類似, 就不貼了, 簡(jiǎn)單貼一下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]];//數(shù)據(jù)格式化
            self.friendCountLabel.text = [NSString stringWithFormat:@"贊 %ld", self.user.friendCount];//數(shù)據(jù)格式化
            ...
        }
    }];
}

@end

UserInfoViewController除了比兩個(gè)TableViewHelper多個(gè)addUI的子控件布局方法, 其他邏輯大同小異, 也是自己管理的MVC, 也是只需要初始化即可在任何一個(gè)Scene中使用.

現(xiàn)在三個(gè)自管理模塊已經(jīng)建立完成, UserVC需要的只是根據(jù)自己的情況做相應(yīng)的拼裝布局即可, 就和搭積木一樣:

@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 {
    
    //這里只是表達(dá)一下意思 具體的layout邏輯肯定不是這么簡(jiǎn)單的
    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 = @"用戶詳情";
//    ...其他設(shè)置
    
    [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模塊不需要任何頁(yè)面加載提示
    [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];//特有部分...
    //...各種設(shè)置
    [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

作為業(yè)務(wù)場(chǎng)景的的Scene(UserVC)做的事情很簡(jiǎn)單, 根據(jù)自身情況對(duì)三個(gè)模塊進(jìn)行配置(configuration), 布局(addUI), 然后通知各個(gè)模塊啟動(dòng)(fetchData)就可以了, 因?yàn)槊總€(gè)模塊的展示和交互是自管理的, 所以Scene只需要負(fù)責(zé)和自身業(yè)務(wù)強(qiáng)相關(guān)的部分即可. 另外, 針對(duì)自身訪問(wèn)的情況我們建立一個(gè)UserVC子類SelfVC, SelfVC做的也是類似的事情.

MVC到這就說(shuō)的差不多了, 對(duì)比上面錯(cuò)誤的MVC方式, 我們看看解決了哪些問(wèn)題:
1.代碼復(fù)用: 三個(gè)小模塊的V(cell/userInfoView)對(duì)外只暴露Set方法, 對(duì)M甚至C都是隔離狀態(tài), 復(fù)用完全沒(méi)有問(wèn)題. 三個(gè)大模塊的MVC也可以用于快速構(gòu)建相似的業(yè)務(wù)場(chǎng)景(大模塊的復(fù)用比小模塊會(huì)差一些, 下文我會(huì)說(shuō)明).
2.代碼臃腫: 因?yàn)镾cene大部分的邏輯和布局都轉(zhuǎn)移到了相應(yīng)的MVC中, 我們僅僅是拼裝MVC的便構(gòu)建了兩個(gè)不同的業(yè)務(wù)場(chǎng)景, 每個(gè)業(yè)務(wù)場(chǎng)景都能正常的進(jìn)行相應(yīng)的數(shù)據(jù)展示, 也有相應(yīng)的邏輯交互, 而完成這些東西, 加空格也就100行代碼左右(當(dāng)然, 這里我忽略了一下Scene的布局代碼).
3.易拓展性: 無(wú)論產(chǎn)品未來(lái)想加回收站還是防御塔, 我需要的只是新建相應(yīng)的MVC模塊, 加到對(duì)應(yīng)的Scene即可.
4.可維護(hù)性: 各個(gè)模塊間職責(zé)分離, 哪里出錯(cuò)改哪里, 完全不影響其他模塊. 另外, 各個(gè)模塊的代碼其實(shí)并不算多, 哪一天即使寫代碼的人離職了, 接手的人根據(jù)錯(cuò)誤提示也能快速定位出錯(cuò)模塊.
5.易測(cè)試性: 很遺憾, 業(yè)務(wù)的初始化依然綁定在Scene的生命周期中, 而有些邏輯也仍然需要UI的點(diǎn)擊事件觸發(fā), 我們依然只能Command+R, 點(diǎn)點(diǎn)點(diǎn)...

3、MVC的缺點(diǎn)

可以看到, 即使是標(biāo)準(zhǔn)的MVC架構(gòu)也并非完美, 仍然有部分問(wèn)題難以解決, 那么MVC的缺點(diǎn)何在? 總結(jié)如下:
1.過(guò)度的注重隔離: 這個(gè)其實(shí)MV(x)系列都有這缺點(diǎn), 為了實(shí)現(xiàn)V層的完全隔離, V對(duì)外只暴露Set方法, 一般情況下沒(méi)什么問(wèn)題, 但是當(dāng)需要設(shè)置的屬性很多時(shí), 大量重復(fù)的Set方法寫起來(lái)還是很累人的.
2.業(yè)務(wù)邏輯和業(yè)務(wù)展示強(qiáng)耦合: 可以看到, 有些業(yè)務(wù)邏輯(頁(yè)面跳轉(zhuǎn)/點(diǎn)贊/分享...)是直接散落在V層的, 這意味著我們?cè)跍y(cè)試這些邏輯時(shí), 必須首先生成對(duì)應(yīng)的V, 然后才能進(jìn)行測(cè)試. 顯然, 這是不合理的. 因?yàn)闃I(yè)務(wù)邏輯最終改變的是數(shù)據(jù)M, 我們的關(guān)注點(diǎn)應(yīng)該在M上, 而不是展示M的V.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末赫舒,一起剝皮案震驚了整個(gè)濱河市脚曾,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌割以,老刑警劉巖金度,帶你破解...
    沈念sama閱讀 221,888評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異严沥,居然都是意外死亡猜极,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門消玄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)跟伏,“玉大人,你說(shuō)我怎么就攤上這事翩瓜∈馨猓” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 168,386評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵兔跌,是天一觀的道長(zhǎng)勘高。 經(jīng)常有香客問(wèn)我,道長(zhǎng)坟桅,這世上最難降的妖魔是什么华望? 我笑而不...
    開(kāi)封第一講書人閱讀 59,726評(píng)論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮仅乓,結(jié)果婚禮上赖舟,老公的妹妹穿的比我還像新娘。我一直安慰自己方灾,他們只是感情好建蹄,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,729評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著裕偿,像睡著了一般洞慎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上嘿棘,一...
    開(kāi)封第一講書人閱讀 52,337評(píng)論 1 310
  • 那天劲腿,我揣著相機(jī)與錄音,去河邊找鬼鸟妙。 笑死焦人,一個(gè)胖子當(dāng)著我的面吹牛挥吵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播花椭,決...
    沈念sama閱讀 40,902評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼忽匈,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了矿辽?” 一聲冷哼從身側(cè)響起丹允,我...
    開(kāi)封第一講書人閱讀 39,807評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎袋倔,沒(méi)想到半個(gè)月后雕蔽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,349評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡宾娜,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,439評(píng)論 3 340
  • 正文 我和宋清朗相戀三年批狐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片前塔。...
    茶點(diǎn)故事閱讀 40,567評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡嚣艇,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出嘱根,到底是詐尸還是另有隱情髓废,我是刑警寧澤,帶...
    沈念sama閱讀 36,242評(píng)論 5 350
  • 正文 年R本政府宣布该抒,位于F島的核電站慌洪,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏凑保。R本人自食惡果不足惜冈爹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,933評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望欧引。 院中可真熱鬧频伤,春花似錦、人聲如沸芝此。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,420評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)婚苹。三九已至岸更,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間膊升,已是汗流浹背怎炊。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,531評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人评肆。 一個(gè)月前我還...
    沈念sama閱讀 48,995評(píng)論 3 377
  • 正文 我出身青樓债查,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親瓜挽。 傳聞我的和親對(duì)象是個(gè)殘疾皇子盹廷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,585評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • 前言 看了下上篇博客的發(fā)表時(shí)間到這篇博客排惨,竟然過(guò)了11個(gè)月吭敢,罪過(guò),罪過(guò)暮芭。這一年時(shí)間也是夠折騰的鹿驼,年初離職跳槽到鵝廠...
    西木柚子閱讀 21,248評(píng)論 12 184
  • 原文 前言 本文為回答一位朋友關(guān)于MVC/MVP/MVVM架構(gòu)方面的疑問(wèn)所寫, 旨在介紹iOS下MVC/MVP/M...
    無(wú)灃閱讀 711評(píng)論 1 2
  • //聯(lián)系人:石虎QQ: 1224614774昵稱:嗡嘛呢叭咪哄 MVC MVC的相關(guān)概念 MVC最早存在于桌面程序...
    石虎132閱讀 2,179評(píng)論 0 9
  • 爸爸生病住院的那些日子,應(yīng)該是我生命中最灰暗的顏色辕宏。那些天過(guò)后畜晰,我就成了沒(méi)有爸爸的孩子。 我和媽媽一起在醫(yī)院陪爸爸...
    凌云是mi閱讀 230評(píng)論 0 0
  • 卅載清夢(mèng)瑞筐,半醉半醒凄鼻。 時(shí)而疏狂,忽又寧?kù)o聚假。 詩(shī)文萬(wàn)卷块蚌,虛名漸盛。 自知斤兩膘格,蘆葦輕輕峭范。 雖有赤膽,雄心認(rèn)慫瘪贱。 難得...
    書山蒼龍閱讀 328評(píng)論 0 1