雜談: MVC/MVP/MVVM (二)
- MVP
MVC的缺點(diǎn)在于并沒有區(qū)分業(yè)務(wù)邏輯和業(yè)務(wù)展示, 這對單元測試很不友好. MVP針對以上缺點(diǎn)做了優(yōu)化, 它將業(yè)務(wù)邏輯和業(yè)務(wù)展示也做了一層隔離, 對應(yīng)的就變成了MVCP. M和V功能不變, 原來的C現(xiàn)在只負(fù)責(zé)布局, 而所有的邏輯全都轉(zhuǎn)移到了P層.
對應(yīng)關(guān)系如圖所示:
業(yè)務(wù)場景沒有變化, 依然是展示三種數(shù)據(jù), 只是三個(gè)MVC替換成了三個(gè)MVP(圖中我只畫了Blog模塊), UserVC負(fù)責(zé)配置三個(gè)MVP(新建各自的VP, 通過VP建立C, C會(huì)負(fù)責(zé)建立VP之間的綁定關(guān)系), 并在合適的時(shí)機(jī)通知各自的P層(之前是通知C層)進(jìn)行數(shù)據(jù)獲取, 各個(gè)P層在獲取到數(shù)據(jù)后進(jìn)行相應(yīng)處理, 處理完成后會(huì)通知綁定的View數(shù)據(jù)有所更新, V收到更新通知后從P獲取格式化好的數(shù)據(jù)進(jìn)行頁面渲染, UserVC***將已經(jīng)渲染好的各個(gè)View進(jìn)行布局即可. 另外, V層C層不再處理任何業(yè)務(wù)邏輯, 所有事件觸發(fā)全部調(diào)用P層的相應(yīng)命令, 具體到代碼中如下:
- @interface BlogPresenter : NSObject
- + (instancetype)instanceWithUserId:(NSUInteger)userId;
- - (NSArray *)allDatas;//業(yè)務(wù)邏輯移到了P層 和業(yè)務(wù)相關(guān)的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;
- }
- //提供給外層調(diào)用的命令
- - (void)refreshUserBlogsWithCompletionHandler:(NetworkTaskCompletionHander)completionHandler {
- [self.apiManager refreshUserBlogsWithUserId:self.userId completionHandler:^(NSError *error, id result) {
- if (!error) {
- [self.blogs removeAllObjects];//清空之前的數(shù)據(jù)
- for (Blog *blog in result) {
- [self.blogs addObject:[BlogCellPresenter presenterWithBlog:blog]];
- }
- }
- completionHandler ? completionHandler(error, result) : nil;
- }];
- }
- //提供給外層調(diào)用的命令
- - (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層, 其實(shí)就是一系列業(yè)務(wù)邏輯的集合. BlogPresenter負(fù)責(zé)獲取Blogs原始數(shù)據(jù)并通過這些原始數(shù)據(jù)構(gòu)造BlogCellPresenter, 而BlogCellPresenter提供格式化好的各種數(shù)據(jù)以供Cell渲染, 另外, 點(diǎn)贊和分享的業(yè)務(wù)現(xiàn)在也轉(zhuǎn)移到了這里.
業(yè)務(wù)邏輯被轉(zhuǎn)移到了P層, 此時(shí)的V層只需要做兩件事:
1.監(jiān)聽P層的數(shù)據(jù)更新通知, 刷新頁面展示.
2.在點(diǎn)擊事件觸發(fā)時(shí), 調(diào)用P層的對應(yīng)方法, 并對方法執(zhí)行結(jié)果進(jìn)行展示.
- @interface BlogCell : UITableViewCell
- @property (strong, nonatomic) BlogCellPresenter *presenter;
- @end
- @implementation BlogCell
- - (void)setPresenter:(BlogCellPresenter *)presenter {
- _presenter = presenter;
- //從Presenter獲取格式化好的數(shù)據(jù)進(jìn)行展示
- 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之間的綁定(這里可能不太明顯, 因?yàn)锽logVC里面的布局代碼是TableViewDataSource, PV綁定的話, 因?yàn)槲彝祽杏昧薆lock做通知回調(diào), 所以也不太明顯, 如果是Protocol回調(diào)就很明顯了), 代碼如下:
- @interface BlogViewController : NSObject
- + (instancetype)instanceWithTableView:(UITableView *)tableView presenter:(BlogPresenter)presenter;
- - (void)setDidSelectRowHandler:(void (^)(Blog *))didSelectRowHandler;
- - (void)fetchDataWithCompletionHandler:(NetworkCompletionHandler)completionHandler;
- @end
BlogViewController現(xiàn)在不再負(fù)責(zé)實(shí)際的數(shù)據(jù)獲取邏輯, 數(shù)據(jù)獲取直接調(diào)用Presenter的相應(yīng)接口, 另外, 因?yàn)闃I(yè)務(wù)邏輯也轉(zhuǎn)移到了Presenter, 所以TableView的布局用的也是Presenter.allDatas. 至于Cell的展示, 我們替換了原來大量的Set方法, 讓Cell自己根據(jù)綁定的CellPresenter做展示. 畢竟現(xiàn)在邏輯都移到了P層, V層要做相應(yīng)的交互也必須依賴對應(yīng)的P層命令, 好在V和M仍然是隔離的, 只是和P耦合了, P層是可以隨意替換的, M顯然不行, 這是一種折中.
***是Scene, 它的變動(dòng)不大, 只是替換配置MVC為配置MVP, 另外數(shù)據(jù)獲取也是走P層, 不走C層了(然而代碼里面并不是這樣的):
- - (void)configuration {
- // ...其他設(shè)置
- 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];
- }];
- //還是因?yàn)閼? 用了Block走C層轉(zhuǎn)發(fā)會(huì)少寫一些代碼, 如果是Protocol或者KVO方式就會(huì)用self.blogViewController.presenter了
- //不過沒有關(guān)系, 因?yàn)槲覀兲鎿QMVC為MVP是為了解決單元測試的問題, 現(xiàn)在的用法完全不影響單元測試, 只是和概念不符罷了.
- // ...略
- }
上面的例子中其實(shí)有一個(gè)問題, 即我們假定: 所有的事件都是由V層主動(dòng)發(fā)起且一次性的. 這其實(shí)是不成立的, 舉個(gè)簡單的例子: 類似微信語音聊天之類的頁面, 點(diǎn)擊語音Cell開始播放, Cell展示播放動(dòng)畫, 播放完成動(dòng)畫停止, 然后播放下一條語音.
在這個(gè)播放場景中, 如果CellPresenter還是像上面一樣僅僅提供一個(gè)playWithCompletionHandler的接口是行不通的. 因?yàn)椴シ磐瓿珊蠡卣{(diào)肯定是在C層, C層在播放完成后會(huì)發(fā)現(xiàn)此時(shí)執(zhí)行播放命令的CellPresenter無法通知Cell停止動(dòng)畫, 即事件的觸發(fā)不是一次性的. 另外, 在播放完成后, C層遍歷到下一個(gè)待播放CellPresenterX調(diào)用播放接口時(shí), CellPresenterX因?yàn)椴⒉恢浪鼘?yīng)的Cell是誰, 當(dāng)然也就無法通知Cell開始動(dòng)畫, 即事件的發(fā)起者并不一定是V層.
針對這些非一次性或者其他層發(fā)起事件, 處理方法其實(shí)很簡單, 在CellPresenter加個(gè)Block屬性就行了, 因?yàn)槭菍傩? Block可以多次回調(diào), 另外Block還可以捕獲Cell, 所以也不擔(dān)心找不到對應(yīng)的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;
- }
- }];
- }
- }
播放的時(shí)候, VC只需要保持一下CellPresenter, 然后傳入相應(yīng)的playState調(diào)用didUpdatePlayStateHandler就可以更新Cell的狀態(tài)了.
當(dāng)然, 如果是Protocol的方式進(jìn)行的VP綁定, 那么做這些事情就很平常了, 就不寫了.
MVP大概就是這個(gè)樣子了, 相對于MVC, 它其實(shí)只做了一件事情, 即分割業(yè)務(wù)展示和業(yè)務(wù)邏輯. 展示和邏輯分開后, 只要我們能保證V在收到P的數(shù)據(jù)更新通知后能正常刷新頁面, 那么整個(gè)業(yè)務(wù)就沒有問題. 因?yàn)閂收到的通知其實(shí)都是來自于P層的數(shù)據(jù)獲取/更新操作, 所以我們只要保證P層的這些操作都是正常的就可以了. 即我們只用測試P層的邏輯, 不必關(guān)心V層的情況.
- MVVM
MVP其實(shí)已經(jīng)是一個(gè)很好的架構(gòu), 幾乎解決了所有已知的問題, 那么為什么還會(huì)有MVVM呢?
仍然是舉例說明, 假設(shè)現(xiàn)在有一個(gè)Cell, 點(diǎn)擊Cell上面的關(guān)注按鈕可以是加關(guān)注, 也可以是取消關(guān)注, 在取消關(guān)注時(shí), SceneA要求先彈窗詢問, 而SceneB則不做彈窗, 那么此時(shí)的取消關(guān)注操作就和業(yè)務(wù)場景強(qiáng)關(guān)聯(lián), 所以這個(gè)接口不可能是V層直接調(diào)用, 會(huì)上升到Scene層.具體到代碼中, 大概這個(gè)樣子:
- @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) {//未關(guān)注 去關(guān)注
- // follow user
- } else {//已關(guān)注 則取消關(guān)注
- self.followStateHander ? self.followStateHander(YES) : nil;//先通知Cell顯示follow狀態(tài)
- [[FollowAPIManager new] unfollowWithUserId:self.userId completionHandler:^(NSError *error, id result) {
- if (error) {
- self.followStateHander ? self.followStateHander(NO) : nil;//follow失敗 狀態(tài)回退
- } 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 {//將關(guān)注按鈕點(diǎn)擊事件上傳
- [self routeEvent:@"followEvent" userInfo:@{@"presenter" : self.presenter}];
- }
- @end
- @implementation FollowListViewController
- //攔截點(diǎn)擊事件 判斷后確認(rèn)是否執(zhí)行事件
- - (void)routeEvent:(NSString *)eventName userInfo:(NSDictionary *)userInfo {
- if ([eventName isEqualToString:@"followEvent"]) {
- UserCellPresenter *presenter = userInfo[@"presenter"];
- [self showAlertWithTitle:@"提示" message:@"確認(rèn)取消對他的關(guān)注嗎?" cancelHandler:nil confirmHandler: ^{
- [presenter follow];
- }];
- }
- }
- @end
- @implementation UIResponder (Router)
- //沿著響應(yīng)者鏈將事件上傳 事件最終被攔截處理 或者 無人處理直接丟棄
- - (void)routeEvent:(NSString *)eventName userInfo:(NSDictionary *)userInfo {
- [self.nextResponder routeEvent:eventName userInfo:userInfo];
- }
- @end
Block方式看起來略顯繁瑣, 我們換到Protocol看看:
- @protocol UserCellPresenterCallBack
- - (void)userCellPresenterDidUpdateFollowState:(BOOL)isFollowing;
- @end
- @interface UserCellPresenter : NSObject
- @property (weak, nonatomic) id view;
- @property (assign, nonatomic) BOOL isFollowing;
- - (void)follow;
- @end
- @implementation UserCellPresenter
- - (void)follow {
- if (!self.isFollowing) {//未關(guān)注 去關(guān)注
- // follow user
- } else {//已關(guān)注 則取消關(guān)注
- 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之類的代碼, 可以發(fā)現(xiàn)無論是Block方式還是Protocol方式因?yàn)樾枰獙撁嬲故竞蜆I(yè)務(wù)邏輯進(jìn)行隔離, 代碼上饒了一小圈, 無形中增添了不少的代碼量, 這里僅僅只是一個(gè)事件就這樣, 如果是多個(gè)呢? 那寫起來真是蠻傷的…
仔細(xì)看一下上面的代碼就會(huì)發(fā)現(xiàn), 如果我們繼續(xù)添加事件, 那么大部分的代碼都是在做一件事情: P層將數(shù)據(jù)更新通知到V層. Block方式會(huì)在P層添加很多屬性, 在V層添加很多設(shè)置Block邏輯. 而Protocol方式雖然P層只添加了一個(gè)屬性, 但是Protocol里面的方法卻會(huì)一直增加, 對應(yīng)的V層也就需要增加的方法實(shí)現(xiàn).
問題既然找到了, 那就試著去解決一下吧, OC中能夠?qū)崿F(xiàn)兩個(gè)對象間的低耦合通信, 除了Block和Protocol, 一般都會(huì)想到KVO. 我們看看KVO在上面的例子有何表現(xiàn):
- @interface UserCellViewModel : NSObject
- @property (assign, nonatomic) BOOL isFollowing;
- - (void)follow;
- @end
- @implementation UserCellViewModel
- - (void)follow {
- if (!self.isFollowing) {//未關(guān)注 去關(guān)注
- // follow user
- } else {//已關(guān)注 則取消關(guān)注
- self.isFollowing = YES;//先通知Cell顯示follow狀態(tài)
- [[FollowAPIManager new] unfollowWithUserId:self.userId completionHandler:^(NSError *error, id result) {
- if (error) { self.isFollowing = NO; }//follow失敗 狀態(tài)回退
- //...略
- }];
- }
- }
- @end
- @implementation UserCell
- - (void)awakeFromNib {
- @weakify(self);
- [RACObserve(self, viewModel.isFollowing) subscribeNext:^(NSNumber *isFollowing) {
- @strongify(self);
- [self.followStateButton setImage:[isFollowing boolValue] ? : ...];
- };
- }
代碼大概少了一半左右, 另外, 邏輯讀起來也清晰多了, Cell觀察綁定的ViewModel的isFollowing狀態(tài), 并在狀態(tài)改變時(shí), 更新自己的展示.
三種數(shù)據(jù)通知方式簡單一比對, 相信哪種方式對程序員更加友好, 大家都心里有數(shù), 就不做贅述了.
現(xiàn)在大概一提到MVVM就會(huì)想到RAC, 但這兩者其實(shí)并沒有什么聯(lián)系, 對于MVVM而言RAC只是提供了優(yōu)雅安全的數(shù)據(jù)綁定方式, 如果不想學(xué)RAC, 自己搞個(gè)KVOHelper之類的東西也是可以的. 另外 ,RAC的魅力其實(shí)在于函數(shù)式響應(yīng)式編程, 我們不應(yīng)該僅僅將它局限于MVVM的應(yīng)用, 日常的開發(fā)中也應(yīng)該多使用使用的.
關(guān)于MVVM, 我想說的就是這么多了, 因?yàn)镸VVM其實(shí)只是MVP的綁定進(jìn)化體, 除去數(shù)據(jù)綁定方式, 其他的和MVP如出一轍, 只是可能呈現(xiàn)方式是Command/Signal而不是CompletionHandler之類的, 故不做贅述.
***做個(gè)簡單的總結(jié)吧:
1.MVC作為老牌架構(gòu), 優(yōu)點(diǎn)在于將業(yè)務(wù)場景按展示數(shù)據(jù)類型劃分出多個(gè)模塊, 每個(gè)模塊中的C層負(fù)責(zé)業(yè)務(wù)邏輯和業(yè)務(wù)展示, 而M和V應(yīng)該是互相隔離的以做重用, 另外每個(gè)模塊處理得當(dāng)也可以作為重用單元. 拆分在于解耦, 順便做了減負(fù), 隔離在于重用, 提升開發(fā)效率. 缺點(diǎn)是沒有區(qū)分業(yè)務(wù)邏輯和業(yè)務(wù)展示, 對單元測試不友好.
2.MVP作為MVC的進(jìn)階版, 提出區(qū)分業(yè)務(wù)邏輯和業(yè)務(wù)展示, 將所有的業(yè)務(wù)邏輯轉(zhuǎn)移到P層, V層接受P層的數(shù)據(jù)更新通知進(jìn)行頁面展示. 優(yōu)點(diǎn)在于良好的分層帶來了友好的單元測試, 缺點(diǎn)在于分層會(huì)讓代碼邏輯優(yōu)點(diǎn)繞, 同時(shí)也帶來了大量的代碼工作, 對程序員不夠友好.
3.MVVM作為集大成者, 通過數(shù)據(jù)綁定做數(shù)據(jù)更新, 減少了大量的代碼工作, 同時(shí)優(yōu)化了代碼邏輯, 只是學(xué)習(xí)成本有點(diǎn)高, 對新手不夠友好.
4.MVP和MVVM因?yàn)榉謱铀詴?huì)建立MVC兩倍以上的文件類, 需要良好的代碼管理方式.
5.在MVP和MVVM中, V和P或者VM之間理論上是多對多的關(guān)系, 不同的布局在相同的邏輯下只需要替換V層, 而相同的布局不同的邏輯只需要替換P或者VM層. 但實(shí)際開發(fā)中P或者VM往往因?yàn)轳詈狭薞層的展示邏輯退化成了一對一關(guān)系(比如SceneA中需要顯示”xxx+Name”, VM就將Name格式化為”xxx + Name”. 某一天SceneB也用到這個(gè)模塊, 所有的點(diǎn)擊事件和頁面展示都一樣, 只是Name展示為”yyy + Name”, 此時(shí)的VM因?yàn)轳詈蟂ceneA的展示邏輯, 就顯得比較尷尬), 針對此類情況, 通常有兩種辦法, 一種是在VM層加狀態(tài)進(jìn)而判斷輸出狀態(tài), 一種是在VM層外再加一層FormatHelper. 前者可能因?yàn)闋顟B(tài)過多顯得代碼難看, 后者雖然比較優(yōu)雅且拓展性高, 但是過多的分層在數(shù)據(jù)還原時(shí)就略顯笨拙, 大家應(yīng)該按需選擇.
這里隨便瞎扯一句, 有些文章上來就說MVVM是為了解決C層臃腫, MVC難以測試的問題, 其實(shí)并不是這樣的. 按照架構(gòu)演進(jìn)順序來看, C層臃腫大部分是沒有拆分好MVC模塊, 好好拆分就行了, 用不著MVVM. 而MVC難以測試也可以用MVP來解決, 只是MVP也并非***, 在VP之間的數(shù)據(jù)交互太繁瑣, 所以才引出了MVVM. 當(dāng)MVVM這個(gè)完全體出現(xiàn)以后, 我們從結(jié)果看起源, 發(fā)現(xiàn)它做了好多事情, 其實(shí)并不是, 它的前輩們付出的努力也并不少!
- 架構(gòu)那么多, 日常開發(fā)中到底該如何選擇?
不管是MVC, MVP, MVVM還是MVXXX, 最終的目的在于服務(wù)于人, 我們注重架構(gòu), 注重分層都是為了開發(fā)效率, 說到底還是為了開心. 所以, 在實(shí)際開發(fā)中不應(yīng)該拘泥于某一種架構(gòu), 根據(jù)實(shí)際項(xiàng)目出發(fā), 一般普通的MVC就能應(yīng)對大部分的開發(fā)需求, 至于MVP和MVVM, 可以嘗試, 但不要強(qiáng)制.
總之, 希望大家能做到: 設(shè)計(jì)時(shí), 心中有數(shù). 擼碼時(shí), 開心就好.