Android 不負責任系列 - Jetpack 組件、MVVM 架構,簡稱 AAC、整潔架構(Clean Architecture) 的領域層(Domain Layer) UseCase 介紹


KunMinX - Jetpack-MVVM-Best-Practice

Alt

Alt

KunMinX 開源的 Jetpack 組件搭配 MVVM 架構又稱 Android Architecture Components 架構簡稱 AAC 架構之 PureMusic 音樂撥放器範例中的 UseCase 介紹。

在眾多開源專案裡第一次看到有人實踐 Uncle Bob 的 Clean Architecture 中的 UseCase 層,非常驚奇,所以想研究一下如何實踐 UseCase。

Interface

  • CallBack

    public interface CallBack<R> {
      void onSuccess(R response);
    
      default void onError() {
      }
    }
    
  • RequestValues
    /**
    * Data passed to a request.
    */
    public interface RequestValues {
    }
    
  • ResponseValue
    /**
    * Data received from a request.
    */
    public interface ResponseValue {
    }
    
  • UseCaseScheduler

    ```
    /**

    • Interface for schedulers, see {@link UseCaseThreadPoolScheduler}.
      */
      public interface UseCaseScheduler {

      void execute(Runnable runnable);

      void notifyResponse(final V response, final CallBack callBack);

      void onError(final CallBack callBack);
      }

## Class
* UseCase
![](https://i.imgur.com/No3p08w.png)

/**

  • Use cases are the entry points to the domain layer.
    *
  • @param the request type
  • @param

    the response type
    */
    public abstract class UseCase<Q extends RequestValues,P extends ResponseValue> {

    private Q mRequestValues;
    private CallBack

    mCallBack;

    public Q getRequestValues() {

     return mRequestValues;
    

    }

    public void setRequestValues(Q mRequestValues) {

     this.mRequestValues = mRequestValues;
    

    }

    public CallBack

    getCallBack() {

     return mCallBack;
    

    }

    public void setCallBack(CallBack

    mCallBack) {

     this.mCallBack = mCallBack;
    

    }

    void run() {

     executeUseCase(mRequestValues);
    

    }

    protected abstract void executeUseCase(Q requestValues);

}

* UseCaseHandler
![](https://i.imgur.com/aKP9SP5.png)

public class UseCaseHandler {

private static UseCaseHandler INSTANCE;

private final UseCaseScheduler mUseCaseScheduler;

public UseCaseHandler(UseCaseScheduler mUseCaseScheduler) {
    this.mUseCaseScheduler = mUseCaseScheduler;
}

public static UseCaseHandler getInstance() {
    if (INSTANCE == null) {
        INSTANCE = new UseCaseHandler(new UseCaseThreadPoolScheduler());
    }
    return INSTANCE;
}

public <T extends RequestValues, R extends ResponseValue> void execute(
        final UseCase<T, R> useCase, T values, CallBack<R> callBack) {
    useCase.setRequestValues(values);
    useCase.setCallBack(new UiCallbackWrapper(callBack, this));
}

private <V extends ResponseValue> void notifyResponse(final V response,
                                                              final CallBack<V> callBack) {
    mUseCaseScheduler.notifyResponse(response, callBack);
}

private <V extends ResponseValue> void notifyError(final CallBack<V> callBack) {
    mUseCaseScheduler.onError(callBack);
}

private static final class UiCallbackWrapper<V extends ResponseValue>
        implements CallBack<V> {
    private final CallBack<V> mCallBack;
    private final UseCaseHandler mUseCaseHandler;

    public UiCallbackWrapper(CallBack<V> mCallBack, UseCaseHandler mUseCaseHandler) {
        this.mCallBack = mCallBack;
        this.mUseCaseHandler = mUseCaseHandler;
    }

    @Override
    public void onSuccess(V response) {
        mUseCaseHandler.notifyResponse(response, mCallBack);
    }

    @Override
    public void onError() {
        mUseCaseHandler.notifyError(mCallBack);
    }
}

}


* UseCaseThreadPoolScheduler
![](https://i.imgur.com/qzAGYOa.png)

/**

  • Executes asynchronous tasks using a {@link ThreadPoolExecutor}.
  • See also {@link Executors} for a list of factory methods to create common
  • {@link java.util.concurrent.ExecutorService}s for different scenarios.
    */
    public class UseCaseThreadPoolScheduler implements UseCaseScheduler {

    public static final int POOL_SIZE = 2;
    public static final int MAX_POOL_SIZE = 4 * 2;
    public static final int FIXED_POOL_SIZE = 4;
    public static final int TIMEOUT = 30;
    final ThreadPoolExecutor mThreadPoolExecutor;
    private final Handler mHandler = new Handler();

    public UseCaseThreadPoolScheduler() {

     mThreadPoolExecutor = new ThreadPoolExecutor(
             FIXED_POOL_SIZE, FIXED_POOL_SIZE,
             TIMEOUT, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
    

    }

    @Override
    public void execute(Runnable runnable) {

     mThreadPoolExecutor.execute(runnable);
    

    }

    @Override
    public void notifyResponse(V response, CallBack callBack) {

     mHandler.post(() -> {
         if (callBack != null) {
             callBack.onSuccess(response);
         }
     });
    

    }

    @Override
    public void onError(CallBack callBack) {

     mHandler.post(callBack::onError);
    

    }
    }

    ## Example
    ### UseCase
    #### CanBeStoppedUseCase
    ![](https://i.imgur.com/i3qbNHE.png)
    

    /**

  • UseCase 示例,實現 LifeCycle 接口,單獨服務於 有 “叫停” 需求 的業務
  • 同樣是“下載”,我不是在數據層分別寫兩個方法,
  • 而是遵循開閉原則,在 ViewModel 和 數據層之間,插入一個 UseCase,來專門負責可叫停的情況,
  • 除了開閉原則,使用 UseCase 還有個考慮就是避免內存洩漏,
    */
    public class CanBeStoppedUseCase extends UseCase<CanBeStoppedUseCase.CanBeStoppedRequestValues,

     CanBeStoppedUseCase.CanBeStoppedResponseValue> implements DefaultLifecycleObserver {
    

    private final DownloadFile mDownloadFile = new DownloadFile();

    @Override
    public void onStop(@NonNull LifecycleOwner owner) {

     if (getRequestValues() != null) {
         mDownloadFile.setForgive(true);
         mDownloadFile.setProgress(0);
         mDownloadFile.setFile(null);
         getCallBack().onError();
     }
    

    }

    @Override
    protected void executeUseCase(CanBeStoppedRequestValues canBeStoppedRequestValues) {

     //訪問數據層資源,在 UseCase 中處理帶叫停性質的業務
    
     DataRepository.getInstance().downloadFile(mDownloadFile, dataResult -> {
        getCallBack().onSuccess(new CanBeStoppedResponseValue(dataResult));
     });
    

    }

    public static final class CanBeStoppedRequestValues implements RequestValues {

    }

    public static final class CanBeStoppedResponseValue implements ResponseValue {

     private final DataResult<DownloadFile> mDataResult;
    
     public CanBeStoppedResponseValue(DataResult<DownloadFile> dataResult) {
         mDataResult = dataResult;
     }
    
     public DataResult<DownloadFile> getDataResult() {
         return mDataResult;
     }
    

    }
    }

    #### DownloadUseCase
    ![](https://i.imgur.com/BTMKNOQ.png)
    

    public class DownloadUseCase extends UseCase<DownloadUseCase.DownloadRequestValues, DownloadUseCase.DownloadResponseValue> {

    @Override
    protected void executeUseCase(DownloadRequestValues requestValues) {

     try {
         URL url = new URL(requestValues.url);
         InputStream is = url.openStream();
         File file = new File(Configs.COVER_PATH, requestValues.path);
         OutputStream os = new FileOutputStream(file);
         byte[] buffer = new byte[1024];
         int len = 0;
         while ((len = is.read(buffer)) > 0) {
             os.write(buffer, 0, len);
         }
         is.close();
         os.close();
    
         getCallBack().onSuccess(new DownloadResponseValue(file));
    
     } catch (IOException e) {
         e.printStackTrace();
     }
    

    }

    public static final class DownloadRequestValues implements RequestValues {

     private String url;
     private String path;
    
     public DownloadRequestValues(String url, String path) {
         this.url = url;
         this.path = path;
     }
    
     public String getUrl() {
         return url;
     }
    
     public void setUrl(String url) {
         this.url = url;
     }
    
     public String getPath() {
         return path;
     }
    
     public void setPath(String path) {
         this.path = path;
     }
    

    }

    public static final class DownloadResponseValue implements ResponseValue {

     private File mFile;
    
     public DownloadResponseValue(File file) {
         mFile = file;
     }
    
     public File getFile() {
         return mFile;
     }
    
     public void setFile(File file) {
         mFile = file;
     }
    

    }
    }

#### UseCaseHandler
* DownloadRequest
![](https://i.imgur.com/VAtKEqy.png)

/**

  • 數據下載 Request
  • TODO tip 1:Request 通常按業務劃分
  • 一個項目中通常存在多個 Request 類,
  • 每個頁面配備的 state-ViewModel 實例可根據業務需要持有多個不同的 Request 實例。
  • request 的職責僅限於 "業務邏輯處理" 和 "Event 分發",不建議在此處理 UI 邏輯,
  • UI 邏輯只適合在 Activity/Fragment 等視圖控制器中完成,是 “數據驅動” 的一部分,
  • 將來升級到 Jetpack Compose 更是如此。

  • */
    public class DownloadRequest extends BaseRequest {

    private final UnPeekLiveData<DataResult> mDownloadFileLiveData = new UnPeekLiveData<>();

    private final UnPeekLiveData<DataResult> mDownloadFileCanBeStoppedLiveData = new UnPeekLiveData<>();

    private final CanBeStoppedUseCase mCanBeStoppedUseCase = new CanBeStoppedUseCase();

    public ProtectedUnPeekLiveData<DataResult> getDownloadFileLiveData() {

     return mDownloadFileLiveData;
    

    }

    public ProtectedUnPeekLiveData<DataResult> getDownloadFileCanBeStoppedLiveData() {

     return mDownloadFileCanBeStoppedLiveData;
    

    }

    public CanBeStoppedUseCase getCanBeStoppedUseCase() {

     return mCanBeStoppedUseCase;
    

    }

    public void requestDownloadFile() {

     DownloadFile downloadFile = new DownloadFile();
     DataRepository.getInstance().downloadFile(downloadFile, mDownloadFileLiveData::postValue);
    

    }

    public void requestCanBeStoppedDownloadFile() {

     UseCaseHandler.getInstance().execute(getCanBeStoppedUseCase(),
         new CanBeStoppedUseCase.RequestValues(), response -> {
             mDownloadFileCanBeStoppedLiveData.setValue(response.getDataResult());
         });
    

    }
    }
    ```
    #### ViewModel

/**
 * 每個頁面都要單獨準備一個 state-ViewModel,
 * 來託管 DataBinding 綁定的臨時狀態,以及視圖控制器重建時狀態的恢復。
 * <p>
 * 此外,state-ViewModel 的職責僅限於 狀態託管,不建議在此處理 UI 邏輯,
 * UI 邏輯只適合在 Activity/Fragment 等視圖控制器中完成,是 “數據驅動” 的一部分,
 * 將來升級到 Jetpack Compose 更是如此。
 */
public class SearchViewModel extends ViewModel {

    public final ObservableField<Integer> progress = new ObservableField<>();

    public final ObservableField<Integer> progress_cancelable = new ObservableField<>();

    public final DownloadRequest downloadRequest = new DownloadRequest();
}

心得感想

腳色簡述

  • UseCaseScheduler
    定義排程者要做的事情。
      * execute : 執行 UseCase。
      * notifyResponse : 通知回應。
      * onError : 偵測到錯誤
    
  • UseCaseThreadPoolScheduler
    排程者 : 使用 ThreadPoolExecutor 來做 UseCase 的排程。
    為什麼使用 ThreadPoolExecutor 而不是 Thread 就好 ?
    因為使用 Thread 有兩個缺點
    1. 每次都會new一個執行緒,執行完後銷燬,不能複用。
    2. 如果系統的併發量剛好比較大,需要大量執行緒,那麼這種每次new的方式會搶資源的。
      而 ThreadPoolExecutor 的好處是可以做到執行緒複用,並且使用盡量少的執行緒去執行更多的任務,效率和效能都相當不錯。
  • UseCaseHandler
    提供 execute 方法負責執行 UseCase。並決定執行結果 onSuccess and onError 的 UI 畫面。
  • UseCase
    實作 UseCase 的核心方法。
    ### 總結
    讓我們再看一下 Clean Architecture

    根據 Uncle Bob 的 Clean Architecture 文章表示

    Use Cases
    The software in this layer contains application specific business rules. It encapsulates and implements all of the use cases of the system. These use cases orchestrate the flow of data to and from the entities, and direct those entities to use their enterprise wide business rules to achieve the goals of the use case.
    We do not expect changes in this layer to affect the entities. We also do not expect this layer to be affected by changes to externalities such as the database, the UI, or any of the common frameworks. This layer is isolated from such concerns.
    We do, however, expect that changes to the operation of the application will affect the use-cases and therefore the software in this layer. If the details of a use-case change, then some code in this layer will certainly be affected.

Pros:

  • 業務邏輯分的很清楚。
  • 重複的Code大幅減少。
  • UseCase 彼此能互相使用,功能重用性提高。
  • UseCase 屬於領域層(Domain Layer)並非過往 Android App 架構而是獨立的一個邏輯層,因此具有獨立性。
  • 各個 UseCase 易於測試。
  • ViewModel 的 LiveData 變數大幅減少

Cons:

  • UseCase class 會越來越多。

Q&A

  • 依Uncle Bob 的出發點來說,核心層應該是要越來越抽象,但是Download Usecase 卻比較像是實作細節,這邊不知道你怎麼認為?
    越核心是越抽象的。
    UseCase(姑且我們就稱它為 BaseUseCase),它的確是抽象的,而 Download UseCase 由於是 DownLoad 的業務邏輯,所以要實作。
  • Request 跟 Response 的設計看起來也可以直接用函式的參數跟回傳值來替代就可以,使用這樣的設計有什麼特別的好處嗎?
    先講講 Response 吧,我回傳的資料如果是 Json 我們可以先用 postman 取得 json 格式,並在 Android Studio 使用 JsonFormat plugin 生成一個 JavaBean , 然後打 API 後的 Response 結果在使用 Gson 轉換該 JavaBean 的物件資料,一般的開發流程式這樣對吧?
    然後資料結構會是
    model
      --> local
      --> remote
          --> JavaBean
    
    當一隻程式有上百的 API 那們都放在一起是不是會很亂呢?
    Request 使用函式參數是比較快沒錯,不過當一個方法有多個函式參數時,該 Refactoring 書就建議建立個 Bean 來放置參數了。
  • 不知道 Dagger 和 Koin 對 lambda type 的支援跟 interface 相比是不是達到同級,如果支援足夠(用法跟 interface 一樣自然)的話轉成 lambda 都沒有大問題
    對於 Model 的想呈現的結果可以使用 lambda,至於 Dagger 是否搭配 UseCase ,如果單一業務邏輯的話,我想應該不需要,不過由於 UseCase 是可以互相組合的,所以配合的 ViewModel 層級的 Request 你使用 Dagger 會是比較恰當的
    ## 參考文獻
  • The Clean Architecture
  • 無瑕的程式碼-整潔的軟體設計與架構篇
  • domain-layer
  • java ThreadPoolExecutor使用方法簡單介紹
    ###### tags: Architecture Pattern Clean Architecture Domain Layer UseCase MVVM Jetpack
#Architecture Pattern #Clean Architecture #Domain Layer #Android #UseCase






你可能感興趣的文章

[MTR04] W2 D10 Array 內建函式及 console.log / return 的差異

[MTR04] W2 D10 Array 內建函式及 console.log / return 的差異

this 與 call() / apply() / bind()

this 與 call() / apply() / bind()

SMACSS

SMACSS






留言討論