對iOS MVP架構的一點理解

前言

最近在作一些重構項目的工做,發現項目中存在一個很是麻煩卻又很常見的問題——「龐大臃腫」的view controller!麻煩的緣由是view controller中包含太多業務處理了,難如下手。而說常見的緣由是由於這種現狀已經成爲不少大型項目的通病了!動不動就上千行,閱讀起來很是不便,後期迭代和維護也很麻煩。git

分析緣由

大體閱讀了一些較爲複雜的view controller,發現主要包括如下緣由:github

  • 分層不清晰,view controller裏面不只包括視圖綁定/更新,還有網絡請求、數據解析,業務迭代邏輯等等...
  • 業務邏輯劃分不明確,基本上都在放在一塊兒,只是簡單用#params來劃分
  • view controller之間相互被引用,遷移性差 ......

怎麼搞

針對上面的緣由分析,接下來就要思考怎麼解決。其實,很簡單,咱們就是要對view controller進行「瘦身」,把一些沒必要要的東西分割出去。重點是如何去分割呢?目前市場上比較流行的架構即是MVC、MVP和MVVM三種了。本來項目採用的即是MVC架構,其實並不是MVC很差,只是分層不清晰的話,就會致使不少問題。因此此次重構就摒除了這種選擇。若採用MVVM架構的話,則須要利用到動態綁定技術。通常會選取ReactiveCocoa做爲動態綁定方案,而它須要必定的學習成本,且定位問題起來也比較麻煩。因此,最終決定採用MVP架構來進行重構。網絡

MVP架構

未重構前的架構圖如上所示,view controller幾乎包含了全部職責處理。架構

如上圖所示,對比原來的view controller,新增了presenter層和service層。將業務邏輯放在presenter層,數據請求/解析放在service層,model層只是做爲實體模型數據。ide

那麼這麼作,有什麼優點呢?post

  • 能夠將業務很好地區分開來,好比若包括業務P1,P2...Pn,那麼咱們能夠將不一樣業務交給不一樣的presenter來處理。固然咱們也不是說一個業務就對應一個presenter,能夠將相同/類似的業務放在同一個presenter中。具體業務的粒度就要視狀況而定了。只不過對比起只放在view controller,咱們如今這麼作,更爲靈活些。

具體怎麼實現

咱們以一個簡單的例子來講明,假設咱們的view controller中包含兩個業務,一個是新聞列表刷新,另外一個是反饋內容上傳(原諒我這腦洞....),具體以下圖所示:學習

view的定義

在大型項目中不少view controller中須要用到不少公共的控件,好比視圖跳轉、加載和提示等。爲此,咱們能夠先定義一個公共的view delegate,而後定義一個BaseViewController去實現它。這樣咱們在後面就能夠很方便去調用這些接口。ui

@protocol JBaseViewDelegate <NSObject>
@required
- (void)pushViewController:(UIViewController *)controller;
- (void)popViewController;
- (void)showToast:(NSString *)text;
- (void)showLoading;
- (void)hideLoading;
@end

@interface JBaseViewController : UIViewController <JBaseViewDelegate>
@end
@implementation JBaseViewController
....
@end
複製代碼

presenter的定義

咱們知道presenter在處理業務邏輯時,須要對視圖進行"操做"(注意:這裏的操做並不是真正去更新視圖,而是通知view去更新)。因此presenter須要對view delegate進行綁定。atom

@interface JBasePresenter<T : id<JBaseViewDelegate>> : NSObject {
    __weak T _view;
}
- (instancetype)initWithView:(T)view;
- (T)view;
@end

@implementation JBasePresenter
- (instancetype)initWithView:(id<JBaseViewDelegate>)view {
    if (self = [super init]) {
        _view = view;
    }
    return self;
}
- (id<JBaseViewDelegate>)view {
    return _view;
}
@end
複製代碼
  • Q1:爲何這裏使用泛型T

咱們使用繼承JBaseViewDelegate的泛型T做爲presenter的視圖,這麼作的目的是能夠保證presenter可使用JBaseViewDelegate中定義的接口方法,而且能夠對其進行擴展,以知足業務要求,具體能夠看後面的NewsViewDelegatespa

  • Q2:這裏爲何不使用成員屬性,而是使用成員變量

很簡單,由於成員屬性沒法使用泛型T表示,只能使用id來表示泛型,若是這麼作會致使子類view delegate的接口方法沒有限制。

新聞列表業務NewsPresenter

這裏簡單地列舉了兩個功能,一個是剛進入界面時,刷新數據,另外一個是下拉刷新界面

@protocol NewsViewDelegate <JBaseViewDelegate>
@required
- (void)onRefreshData:(NSArray<NewsModel *> *)data;
@end

@interface NewsPresenter : JBasePresenter<id<NewsViewDelegate>>
- (void)loadData;
- (void)refreshData;
@end
複製代碼
  • Q:爲何NewsPresenter後面要實現id<NewsViewDelegate>

咱們回到以前BasePresenter的定義:<T : id<JBaseViewDelegate>>,因此這裏的id<NewsViewDelegate>其實就是這裏的泛型T。

@interface NewsPresenter()
@property (nonatomic, strong) NewsService *service;
@end

@implementation NewsPresenter
- (void)loadData {
    [self.view showLoading];
    __weak typeof(self) weakSelf = self;
    [self.service loadNewsDataWithCompletion:^(NSArray<NewsModel *> *data) {
        [weakSelf.view hideLoading];
        [weakSelf.view onRefreshData:data];
    }];
}

- (void)refreshData {
    [self.view showLoading];
    __weak typeof(self) weakSelf = self;
    [self.service refreshNewsDataWithCompletion:^(NSArray<NewsModel *> *data) {
        [weakSelf.view hideLoading];
        [weakSelf.view onRefreshData:data];
    }];
}

#pragma mark - getter
- (NewsService *)service {
    if (!_service) {
        _service = [NewsService new];
    }
    return _service;
}
@end
複製代碼

如上所示,相關的業務邏輯和網絡請求都是在這裏執行,固然這裏的網絡請求並非「真正」的網絡請求,具體的網路請求是交給service層去處理的。返回結果以後,就能夠經過view delegate接口去通知view去作相關更新工做。

反饋業務FeedbackPresenter

這裏簡單模擬反饋業務,用戶輸入內容,提交以後,最後響應結果。

@interface FeedbackPresenter : JBasePresenter
- (void)submitFeedback:(NSString *)text;
@end

@interface FeedbackPresenter ()
@property (nonatomic, strong) FeedbackService *service;
@end
@implementation FeedbackPresenter
- (void)submitFeedback:(NSString *)text {
    if (!text || text.length == 0) {
        [self.view showToast:@"輸入的內容爲空!!!"];
        return;
    }
    [self.view showLoading];
    [self.service postFeedback:text completion:^(BOOL succeed) {
        [self.view hideLoading];
        if (succeed) {
            [self.view showToast:@"上傳成功"];
        } else {
            [self.view showToast:@"上傳失敗"];
        }
    }];
}
- (FeedbackService *)service {
    if (!_service) {
        _service = [[FeedbackService alloc] init];
    }
    return _service;
}
@end
複製代碼
  • Q:爲何這裏的FeedbackPresenter後面不須要實現協議呢?

由於反饋業務中並不須要對JBaseViewDelegate進行擴展,因此這裏不須要額外實現擴展的view delegate。默認會綁定JBaseViewDelegate視圖。

Controller

咱們將業務邏輯交給presenter,網絡請求交給service,那麼這時controller的職責就變得不多了。只需負責視圖初始化和實現view delegate的接口,以及對視圖的交互事件委託給presenter處理。

@interface ViewController : JBaseViewController
@end

@interface ViewController () <UITableViewDelegate, UITableViewDataSource, NewsViewDelegate>
....
@property (nonatomic, strong) NewsPresenter *newsPresenter;
@property (nonatomic, strong) FeedbackPresenter *feedbackPresenter;
@end
....
#pragma mark - NewsViewDelegate
- (void)onRefreshData:(NSArray<NewsModel *> *)data {
    if (!self.dataSource) {
        self.dataSource = [NSMutableArray array];
    }
    [self.dataSource addObjectsFromArray:data];
    [self.tableView reloadData];
}

#pragma mark - event
- (void)pullToRefresh {
    [self.refresh endRefreshing];
    [self.newsPresenter refreshData];
}

- (void)sendBtnDidClick:(UIButton *)btn {
    [self.feedbackPresenter submitFeedback:self.textView.text];
}

#pragma mark - getter
- (NewsPresenter *)newsPresenter {
    if (!_newsPresenter) {
        _newsPresenter = [[NewsPresenter alloc] initWithView:self];
    }
    return _newsPresenter;
}
- (FeedbackPresenter *)feedbackPresenter {
    if (!_feedbackPresenter) {
        _feedbackPresenter = [[FeedbackPresenter alloc] initWithView:self];
    }
    return _feedbackPresenter;
}
@end
複製代碼

小結

對比使用MVC架構,MVP架構能比較好地對業務進行劃分,好比上面的兩個業務例子,若使用MVC,那麼勢必會致使兩個業務都會放置在view controller中。而隨着業務的增長,controller就會變得很是「臃腫」了。固然MVP也存在一些劣勢,好比接口和類的數量會增長,但我的以爲對比起愈來愈難維護的controller來講,這也是值得的。此外,使用MVP比較讓人頭疼的一點,就是如何劃分presenter,粒度太小會致使presenter過多,粒度過大,又會致使presenter過於「臃腫」。因此如何把握好粒度的劃分也是要花時間去思考的。

最後附上Demo地址

相關文章
相關標籤/搜索