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);

    <V extends ResponseValue> void notifyResponse(final V response, final CallBack<V> callBack);

    <V extends ResponseValue> void onError(final CallBack<V> callBack);
}

Class

  • UseCase
/**
 * Use cases are the entry points to the domain layer.
 *
 * @param <Q> the request type
 * @param <P> the response type
 */
public abstract class UseCase<Q extends RequestValues,P extends ResponseValue> {

    private Q mRequestValues;
    private CallBack<P> mCallBack;

    public Q getRequestValues() {
        return mRequestValues;
    }

    public void setRequestValues(Q mRequestValues) {
        this.mRequestValues = mRequestValues;
    }

    public CallBack<P> getCallBack() {
        return mCallBack;
    }

    public void setCallBack(CallBack<P> mCallBack) {
        this.mCallBack = mCallBack;
    }

    void run() {
        executeUseCase(mRequestValues);
    }

    protected abstract void executeUseCase(Q requestValues);

}
  • UseCaseHandler
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
/**
 * Executes asynchronous tasks using a {@link ThreadPoolExecutor}.
 * <p>
 * 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 <V extends ResponseValue> void notifyResponse(V response, CallBack<V> callBack) {
        mHandler.post(() -> {
            if (callBack != null) {
                callBack.onSuccess(response);
            }
        });
    }

    @Override
    public <V extends ResponseValue> void onError(CallBack<V> callBack) {
        mHandler.post(callBack::onError);
    }
}

Example

UseCase

CanBeStoppedUseCase

/**
 * 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

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
/**
 * 数据下载 Request
 * <p>
 * TODO tip 1:Request 通常按业务划分
 * 一个项目中通常存在多个 Request 类,
 * 每个页面配备的 state-ViewModel 实例可根据业务需要持有多个不同的 Request 实例。
 * <p>
 * request 的职责仅限於 "业务逻辑处理" 和 "Event 分发",不建议在此处理 UI 逻辑,
 * UI 逻辑只适合在 Activity/Fragment 等视图控制器中完成,是 “数据驱动” 的一部分,
 * 将来升级到 Jetpack Compose 更是如此。
 * <p>
 */
public class DownloadRequest extends BaseRequest {

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

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

    private final CanBeStoppedUseCase mCanBeStoppedUseCase = new CanBeStoppedUseCase();

    public ProtectedUnPeekLiveData<DataResult<DownloadFile>> getDownloadFileLiveData() {
        return mDownloadFileLiveData;
    }

    public ProtectedUnPeekLiveData<DataResult<DownloadFile>> 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 会是比较恰当的

参考文献

tags: Architecture Pattern Clean Architecture Domain Layer UseCase MVVM Jetpack

<<:  MacOS读取蓝牙摇杆讯号,利用python修改pynput程序码实现 - 3.修改pynput

>>:  2022新年挑战 - 7 days for Javascript(Day 1 - Developer Set Up)

沟通这回事:个人经验篇

前言 Hi,铁人赛第二天,跟大家聊聊沟通,预计会陆续写几篇相关的主题,今天来分享平时的观察。 在敏捷...

Day.6 线性资料

线性的资料储存方式一般有两种 array (阵列) list node (链结) 这两种差别到底在那...

第一次参加铁人赛

笔者在这几个事件的时间线上有点错乱,但没关系,大家当故事听听就好了 XDDD 还记得我第一次参加 I...

[DAY 02] Google Apps Script

要操控google 的档案如google drive, google sheet, ...等 你除了...

{CMoney战斗营} 的第七周 # 勉强堪用(?)的重力系统

本周的目标是要让横向卷轴中的角色可以左右移动及跳跃, 在没有碰到场景物件时自由落体, 碰到墙壁时被...