MVP那些事兒(7)……Repository設計分析

不能用真名發表於2018-05-14

目錄

MVP那些事兒(1)……用場景說話

MVP那些事兒(2)……MVC架構初探

MVP那些事兒(3)……在Android中使用MVC(上)

MVP那些事兒(4)……在Android中使用MVC(下)

MVP那些事兒(5)……中介者模式與MVP的關係

MVP那些事兒(6)……MVC變身為MVP

MVP那些事兒(7)……Repository設計分析

幾天前Google IO大會剛剛落下帷幕,相信這又會在技術圈裡掀起一陣浪潮,不得不說,Google對Android的熱情不曾消減,這對我們來說可是一如既往的暖心,畢竟這顆大樹養育了不少產業,廢話不多說,帶著這股暖意我們開啟本章的內容。

在此之前,感謝一下讀者,在第四章節文章裡找出了一處筆誤,證明大家認真的去思考了,在此真心感謝,同時錯誤已經在評論區裡糾正。

在開始之前,還是希望大家能閱讀一下之前的文章,雖然每一章都有一個重點,但這終究是一個體系的學習,在上一章MVP那些事兒(6)……MVC變身為MVP裡,已經為大家講解了MVP整個框架的搭建思路,那麼在這一章中,讓它變的豐滿一些,讓它有一些實際的用途。

目前主流的網路層實現是基於RxJava和Retrofit,大家估計已經耳根子都被這倆個傢伙磨破了,放心,這裡我不會講解他們的具體使用,那麼在我們開發中,網路請求是一個非常重要的環節,在不使用框架時,我們依舊可以使用Retrofit快速的開發出網路請求的業務程式碼,而我們一旦使用了框架,就要考慮,如何將它們這些工具整合到我們的框架中,這可不是簡單的引用和封裝,本章會介紹如何設計一個通用的Repository。

Repository的設計

在Google官方的示例中,Model層被具象成為Repository倉庫,相信很多小夥伴都有看過todo的Demo,包括我之前的文章,命名方式也是沿用了google,這樣大家在理解類的含義的學習成本會變的很低,而更多的精力去關注文章本身所要表達的內容,所以在這裡我們也用Google的思想去設計Model層和它的實現:

我們通過一個類圖來表示:

MVP那些事兒(7)……Repository設計分析

知識點:裝飾設計模式,賦能與遞迴

很顯然,Repository的設計採用了裝飾設計模式,對於裝飾設計模式,它的關鍵詞就是“賦能”,(這詞最近很流行)但這個賦能並不是對本體直接進行賦能,而是創造出來一個新的外殼,這個外殼獲取到本體的能力後,可以對本體能力進行演化,而這個外殼也會把演化後的能力對外暴露,讓其他外殼對自己的能力進行演化,還有一種情況,外殼並不是演化本體功能,而是又附加了一個全新的功能,想象一下你的手機加了一個保護殼,又加了一個外接電源(對原有電量擴充),又加了一個手柄殼(全新功能)。本體可以有多個,而基於本體的外殼可以有多個,而一個外殼再其他的外殼眼中也可能是個本體,想想都很酷炫,而這樣的既能縱向又可橫向的擴充套件要比傳統的單繼承和多實現來的舒服的多。關於遞迴,裝飾模式本身也是類遞迴的體現,這裡不詳細闡述。

在Android的開發中我們一定會去伺服器獲取資料和資源,再結合實際的需要是否需要把這些資料快取在本地,或者對本地資料進行一些操作,而這些功能的統一出口就是Repository,所以前期設計只需考慮這兩個方向,本地和遠端,別忘了由於使用的是裝飾設計模式,在未來我們可以根據具體業務來建立出他們的演化版本,而無需汙染本體,我們完全可以獨立這塊業務,而使用Repository的使用者,可以完全不用關心這些,這其實也是分層架構的魅力。接下來我們通過一個簡單的示例去講解這部分內容。

具體設計與實現,讓我們來設計一個Repository

首先,我們定義一個DataSource介面,它只有一個職責getObject(),DataSource 可以看作為頂級介面,它是一切的起源(始祖)。

1、定義DataSource

public interface DataSource {
    void getOject();
}
複製程式碼

基於這個始祖我們需要定義兩個子類,Remote和Local,我們先忽略他們的getObject()的具體實現(涉及到具體實現可能會干擾對設計理解),你可以簡單的想象一下,一個是從遠端獲取資料, 一個是從本地獲取資料:

2、實現一個RemoteDataSource和LocalDataSource

/**
*   Remote
**/
public RemoteDataSource  implements DataSource{
    void getInstance(){}
    void getOject(){
        //具體實現
    }
}
/**
*   Local
**/
public LocalDataSource  implements DataSource{
    void getInstance(){}
    void getOject() {
        //具體實現
    }
}
複製程式碼

按照設計,Repository的整體設計思路主要是裝飾模式,而目前為止只看到了實現,並沒有所謂的“賦能”,所以我們這裡還缺失一個主要的角色Repository,它如何去設計?

對外提供統一介面

我們在開發過程中,經常能聽到這樣的建議:請對外提供一個統一的介面,這樣以後有改動只需要改動內部邏輯,外部引用就不用改了,同時考慮到具體業務:資料的獲取可以從遠到或者是本地獲取,那麼什麼時候從遠端獲取?什麼時候從本地獲取?拿去整合在業務程式碼中的開發兄弟會去關心嗎?他們只關心,如果這塊的業務邏輯變了,他們要不要修改程式碼?這個時候你會很負責的告訴他們,你們無需關心,因為我提供了一個統一介面Repository,業務層開發的兄弟你們只要關心在什麼時機去初始化和呼叫就好。

3、實現Repository

按照上面的分析,1、讓Repository繼承始祖DataSource,2、讓Repository依賴於 RemoteDataSource和LocalDataSource:

public class Repository implements DataSource{

    private LocalDataSource local;
    private RemoteDataSource remote;
    
    void getInstance(DataSource remote, DataSourc, local){
        this.remote = remote;
        this.local = local;
    }
    
    @Override
    void getOject() {
        if(//邏輯判斷) {
            local.getObject();
        } else {
            remote.getObject();
        }
    }
}
複製程式碼

首先Repository也是一個DataSource,它同樣擁有getOject()的能力,而它對getOject()方法的處理,其實是借用了local和remote的能力,換個角度來看Repository就是remote和local的包裝類,它做的只是實現了某部分業務邏輯:即,什麼時候呼叫local和什麼時候呼叫remote。由於Repository是對外統一介面物件,所以當內部業務邏輯發生改變,外部是無需關心的。

為了Repository使用方便,我們建立一個靜態工廠類RepositoryInjection

public class RepositoryInjection {

    public static Repository provideRepository(@NonNull Context context) {
        checkNotNull(context);
        return Repository.getInstance(RemoteDataSource.getInstance(context),
                LocalDataSource.getInstance(context));
    }
}
複製程式碼

那麼業務開發的同學在合適的地方可以這樣呼叫:

RepositoryInjection.provideRepository(context).getObject();
複製程式碼

此時這個getObject方法到底是remote的還是local的,亦或者是阿貓阿狗的,對於業務來說無需關心。

通過對Repository的分析與實現,我們瞭解了在特定的業務場景下使用裝飾的好處,那麼為了加深理解,我們再假設一個場景,嘗試著去擴充套件一下。

案例場景描述

假設我們的remote已經滿足了基本的網路請求功能,並正式投入到線上,這個時候安全部門報了一個漏洞,說我們的加密key在專案裡使用了明文硬編碼,可能會被獲取,這個時候為了緊急修復,我們選擇通過介面動態獲取安全key來增加安全級別,也就是說在請求所有的資料前都需要先請求一下key,這個簡單的需求我們可以直接改動remote的邏輯如下:

 /**
*   Remote
**/
public RemoteDataSource  implements DataSource{
    void getInstance(){}
    void getOject(){
        //1、先獲取key
        getKey();
        //2、再獲取資料
        getData()
    }
}
複製程式碼

你很快的完成了這個需求並沾沾自喜,上線執行,過幾天技術經理髮現了一個問題,由於介面訪問量太大,資源消耗嚴重,決定只對部分介面進行加密,這個時候remote可能無法獨立去完成這個需求,就需要擴充套件了,如何擴充套件?我們可以這樣:

首先,把RemoteDataSource改為最初的版本。

 /**
*   Remote
**/
public RemoteDataSource  implements DataSource{
    void getInstance(){}
    void getOject(){
        //獲取資料
        getData()
    }
}
複製程式碼

其次,定義一個RemoteKeyDataSource類對RemoteDataSource進行包裝

 /**
*   New Remote key
**/
public RemoteKeyDataSource  implements DataSource{
    private RemoteDataSource remote;
    void getInstance(DataSource remote){
        this.remote = remote;
    }
    void getOject(){
        //獲取key
        getKey();
        //獲取資料
        remote.getObject();
    }
}
複製程式碼

我們演化的getObject方法首先執行getKey()獲取加密key,然後執行remote的getObject()方法,這樣在不修改remote的前提下功能得到了演化。

How to use

修改一下Repository

public class Repository implements DataSource{

    private LocalDataSource local;
    private RemoteDataSource remote;
    
    void getInstance(DataSource remote, DataSourc, local){
        this.remote = remote;
        this.local = local;
    }
    
    @Override
    void getOject() {
        if(//邏輯判斷) {
            local.getObject();
        } else if(//需要key){
            //包裝一下remote
            RemoteKeyDataSource.getInstance(remote).getObject();
        } eles {
            //不需要要key
            remote.getObject();
        }
    }
}
複製程式碼

而對於業務方的同學,他們對這次修改是“無感知”的。如果有一天業務場景發生了變化要用回RemoteDataSource,相信那個時候你一定會從容不迫。

總結

在真實的業務場景中,功能擴充套件是永遠不變的話題,其真實模式很好理解,設計也很好去揣摩,但如何去合理的拆解功能是需要大量的經驗去堆砌的,我們通過裝飾模式來嘗試去分析Repository的設計核心思想,合理的功能劃分加上正確的擴充思路是必不可少的,希望通過這章內容給大家做一個拋磚引玉的作用,設計就是這樣,沒有固定的正規化,但一定要掌握其本質。

在Android中也有裝飾模式的身影,比如ContextWrapper對Context的包裝。那麼Repository設計就僅限於此嗎?答案是否定的,一個通用的框架可考慮的點還是很多的,希望能在未來的章節中有機會就Repository的設計增加幾個場景。

相關文章