帶你動手實現 MVP+Clean架構!

JD-CP發表於2019-03-03

MVP + Clean

Clean 架構,有的同學可能有所耳聞。肯定也有相當一部分同學沒聽說過 Clean 架構。

本篇文章重要講解的是 Clean,MVP 在這裡就不再贅述,感興趣的戳下方連結。

從 0 到 1,帶你解剖 MVP 的神祕之處,並自己動手實現 MVP !

那麼先來解釋一下,何為 Clean?

概念

Clean,中文意思為清潔的、整齊的。所以也可以稱其為 "清晰架構"

它是一種分層架構方式,將 presentation 層(實現層)、

data 層(資料層)以及domain 層(業務邏輯層),彼此獨立。

不同層之間,通過介面來連線,卻又不瞭解彼此的具體實現。

為什麼清晰?因為它有五大特性:

  • 框架獨立
  • 可測試性
  • UI 獨立
  • 資料庫獨立
  • 任何外部代理模組獨立

放一張圖片,感受一下,Clean 的獨特魅力:

這裡寫圖片描述

是不是感覺很方很晃眼?哈哈哈,沒關係,我們只需要理解 presentation、domain、data 三層就可以。

結構

  1. presentation

    在這一層,你可以使用 MVC,也可以使用 MVP,或者 MVVM。

    注意,在這一層,只做與 UI 相關的邏輯。如果你使用 MVP,那麼,每一個 Presenter,

    都至少會有一個 UseCase 組成,他們的主要任務是執行非同步任務,獲取資料,

    並通過回撥拿到資料,供 UI 渲染。

    由此可見,UseCase 的出現,很大程度上釋放了 Presenter。

  2. domain

    業務邏輯處理層。與外部的互動,通過介面實現。

    簡單理解,專案中的業務會在這一層中,一個一個的會被抽離出來。

    抽離出來,交給誰處理呢?當然是 UseCase,presentation 層需要哪種業務,

    就通過 UseCase 呼叫就可以了。

  3. data

    這一層,也很好理解,就是獲取資料的地方。

    每個業務會對應一個 Repository,具體獲取資料的操作就在這裡。

    外部與 data 層連線,是通過 Resporitory。外部不需要知道資料是從哪裡獲取,

    不管是從本地,還是從網路,外部也不需要關心。

需求假設

使用者點選 Button 按鈕,獲取微信精選文章,在介面顯示。

需求拆分:

獲取微信精選文章,可以定義為一個業務。

在 presentation 層,點選了 Button 按鈕,向 Presenter 層,發出獲取文章指令 getArticleList()

Presenter 層收到指令,呼叫 UseCase.execute(),這時就與 domain 層產生聯絡了。

在 execute 方法中,通過 Repository 介面從 data 層獲取資料。

獲取資料後,再通過回撥,最終 presentation 層拿到資料,進行文章列表展示。

這裡寫圖片描述

程式碼實現

建立 ArticleView

public interface ArticleView extends IView {

    void getArticleSuccess(List<ArticleBean> articleBeanList);

    void getArticleFail(String failMsg);

}
複製程式碼

建立 ArticlePresenter

public class ArticlePresenter extends BasePresenter<ArticleView> {

    public void getArticleList(String key) {
        
    }

}
複製程式碼

上面我們說過,Presenter 層是由一個或多個 UseCase 組成,

他們的主要任務是執行非同步任務,獲取資料。那麼我們在 domain 層定義一個 UseCase

名為 ArticleCase,詳細程式碼如下:

public class ArticleCase extends UseCase<List<ArticleBean>, String> {

    private ArticleRepository articleRepository;

    public ArticleCase(ArticleRepository repository) {
        this.articleRepository= repository;
    }

    @Override
    protected Observable<List<ArticleBean>> buildObservable(String s) {
        return articleRepository.getArticleList(s);
    }

}
複製程式碼

UseCase 是封裝好的一個 Case 基類:

/**
 * Case 基類
 *
 * @param <T>      返回資料
 * @param <Params> 請求引數
 */
public abstract class UseCase<T, Params> {

    private final CompositeDisposable mDisposables;

    public UseCase() {
        this.mDisposables = new CompositeDisposable();
    }

    @SuppressLint("CheckResult")
    public void execute(Params params, Consumer nextConsumer, Consumer errorConsumer) {
        Observable<T> observable = this.buildObservable(params);
        addDisposable(observable.subscribe(nextConsumer, errorConsumer));
    }

    protected abstract Observable<T> buildObservable(Params params);

    private void addDisposable(Disposable disposable) {
        mDisposables.add(disposable);
    }

    @SuppressLint("CheckResult")
    public void execute(Params params, BaseObserver<T> observer) {
        Observable<T> observable = this.buildObservable(params);
        observable.subscribe(observer);
        addDisposable(observer.getDisposable());
    }

    public void dispose() {
        if (!mDisposables.isDisposed()) {
            mDisposables.dispose();
        }
    }

}
複製程式碼

任何一個業務類,都需要去繼承 UseCase,並實現 buildObservable 方法。

繼續看 ArticleCase,我們用到了介面 ArticleRepository,很明顯,

這個介面用於被 data 層實現,從而獲取資料並回撥。

public interface ArticleRepository {

    Observable<List<ArticleBean>> getArticleList(String param);

}
複製程式碼

接下來在 data 層,去實現 ArticleRepository

public class ArticleRepositoryImpl implements ArticleRepository {

    private DataApi mApi;

    public ArticleRepositoryImpl() {
        mApi = JDHttp.createApi(DataApi.class);
    }

    @Override
    public Observable<List<ArticleBean>> getArticleList(String param) {
        return mApi.getArticleList(param).compose(JDTransformer.<BaseResponse>switchSchedulers())
                .map(new Function<BaseResponse, List<ArticleBean>>() {
                    @Override
                    public List<ArticleBean> apply(BaseResponse baseResponse) throws Exception {
                        return baseResponse.getResult().getList();
                    }
                });
    }
}
複製程式碼

在這裡,進行了獲取資料操作。無論是從網路、還是本地獲取,domain 層不需要知道。

然後在 Presenter 層中實現 ArticleCase,並呼叫 execute()方法,獲取資料。

public class ArticlePresenter extends BasePresenter<ArticleView> {

    private ArticleCase mCase;

    public void getData(String key) {
        mCase.execute(key, new BaseObserver<List<ArticleBean>>() {
            @Override
            public void onSuccess(List<ArticleBean> articleBeanList) {
                getView().getArticleSuccess(articleBeanList);
            }

            @Override
            public void onFail(String failMsg) {
                getView().getArticleFail(failMsg);
            }
        });
    }

    @Override
    public List<UseCase> createCases() {
        mCase = new ArticleCase(new ArticleRepositoryImpl());
        mCaseList.add(mCase);
        return mCaseList;
    }
}
複製程式碼

並且在 BasePresenter 中實現自動取消訂閱:

public abstract class BasePresenter<V extends IView> implements IPresenter<V> {

    private V mView;

    private CPBridge mBridge = new CPBridge();
    
    protected List<UseCase> mCaseList = new ArrayList<>();

    @RequiresApi(api = Build.VERSION_CODES.N)
    @Override
    public void attach(V view) {
        this.mView = view;
        createCases();
        mCaseList.forEach(new Consumer<UseCase>() {
            @Override
            public void accept(UseCase useCase) {
                mBridge.addCase(useCase);
            }
        });
    }

    @RequiresApi(api = Build.VERSION_CODES.N)
    @Override
    public void detach() {
        this.mView = null;
        mBridge.removeCase();
    }

    protected abstract List<UseCase> createCases();

    public V getView() {
        if (isViewAttached()) {
            return mView;
        } else {
            throw new IllegalStateException("Please call IPresenter.attach(IView view) before requesting data");
        }
    }

    private boolean isViewAttached() {
        return null != mView;
    }

}
複製程式碼

感覺繞嗎?沒關係,結合簡單的業務,將程式碼反覆的敲幾次,你就明白了,相信我。

具體程式碼實現請看 demo。

總結

通過上面的程式碼,實現一個業務,雖然寫了好多類,好多程式碼。

但是這樣寫的好處也是顯而易見的:整體架構更加清晰、易維護、方便測試、高內聚、低耦合。

同時也希望閱讀過這篇文章的人,可以親手實踐。通過實踐,你會明白很多之前不明白的問題。

希望這篇文章能對你有用,謝謝。

最後,附上 demo 地址:MVP-Clean-Demo

您的 star 是我前進的動力,歡迎 star!

相關文章