从Google Sample的Model层设计开始

最近准备动手写个新项目,在构思项目结构的同时,又翻出了google的设计模式Demo回忆了一遍。
之前只是学习Mvp大概结构,没有细究,这次又看了一遍,又有很大的收获。
尤其是在Model层的结构上,又给了自己许多灵感。不得不佩服google的工程师,就算是个demo,各种功能逻辑也是设计的相当规范。

文末附上google设计模式系列demo链接

为什么Model层如此重要

如标题,准备详细记录一下Todo-MVP中Model层的结构和设计流程,就不太多描述其它设计模式相关的内容了。简单介绍下。

Model是 MVP模式中的 M(同MVC中的M)。即模型层,通常用来处理数据库,以及其它数据相关的操作。

发现很多初次接触设计模式的同学往往会纠结各层级的关系,尤其是到了MVP,简单的问题反而多写了许多类。非但没体会到设计模式的优势,还徒增烦恼。其实没必要纠结,通常先理清了单一层所做的事情,其它层级的分工也很容易去理解了。理解了Model层的设计理念,也有助于理解设计模式中各个层级的业务和关系

先上一张今天主要要学习的Todo-MVP官方结构图。

用红色框住的部分是今天的主角Model层

看吧,其实就按Modle层在图中占得比重,那也是妥妥的主角啊!不仅如此,从MVC到MVP,再到MVVM,改朝换代,总是有Model层立足之地。Model层的独立,在任何设计模式中都是相当重要的一部分

Google Sample中的Model层结构

撇开右边的View和Presenter,来看看体积庞大的Model主要是怎么构成的。嗯,体积庞大其实也就三个部分而已,名称标注简单明了,我就翻译凑个字数了。

  1. Remote Data Source 远程数据源
  2. Local Data Source 本地数据源 使用SQLite管理
  3. Repository 仓库,管理调用远程和本地数据,在内存中

而根据结构图中的箭头, 我们可以看出Repository直接和Presenter接触,算是扛起了了Modle层的大旗。统一所有的数据操作,并提供一个Repository来与外界交互,这个大概就是Model层的实现思路了。这样的好处是能把数据相关的操作完全拆分出来,作为一个独立的模块,结构清晰的同时还具有非常高的复用性。

Google Sample中Model层的优势

实际上关于Model层的划分,大家应该都略有心得。对于Slqite,通常都会针对性封装一个操作类,在Activity中更好的调用。而RemoteData,实际上在之前的开发中,确实没有将Local和Remote统一管理的经验和意识,只是针对网络请求进行一定的封装,没有能够深入到Model的层次。这大概是我在这个demo中比较大的收获了。

举个简单例子比如我们有一个Activity展示列表数据,每次打开的时候先直接展示缓存的本地数据,然后有网络的话,马上向服务器请求数据,如果有新的就刷新页面,更新本地缓存,没有,就不做变化。

假设按我之前的思路去处理,大概会有一个LocalDataUtil.getList()来做本地数据的获取,然后还有一个HttpUtil.requestList(),嗯,通常这里是回调获取数据。这样,我们的Activity已经依赖了两个对象,LocalDataUtil和HttpUtil,我们还有在Activity中先 setLocalData(),然后HttpCallback回调结果了,判断是否需要刷新 isNeedUpdate(),最后才来refreshAdapter()刷新一下页面。

补充好业务中的细节处理,也许还会洋洋自得写出了一份优质代码。似乎功能划分很清晰,却没想到其实还可以做到更好。

多学习思考,眼界的开阔和经验的丰富往往会让你有更多更好的灵感。编程无止境。

这时候,大概就能体会到Model层完全独立,将Local和remote统一管理的好处了,不论前端如何变化,到了数据,不外乎增删查改,管你怎么展示数据修改界面和载体,从Activity切到Fragment,Model层只需要提供一个Repository.getList()就行了,不需要携家带口来一次大迁移以及承担代码变动潜藏的新隐患。

Google Sample中Modle的实现细节

先贴图,todo-mvp中modle层的完整结构。

Model层整体在data包下,也算是体现了model负责处理数据相关内容的特性。
其下是Task,这个使我们需要操作的数据对象。然后子包Source里,就是模型层操作相关的内容了。我们可以很轻松的和之前的结构图对应起来。

Local ,Remote 以及Repository。

这里我们可以先把Local包中的TasksDBHelper和TasksPersistenceContract先排除开来。如类名所示,前者继承SQLiteOpenHelper,用于数据库的初始化和建表。后者名称大概可以理解为数据持久化契约,其中定义了一个继承BaseColums的抽象静态内部类TaskEntry,对应Task对象用于描述表对象的列名。

之后,我们可以看到有类TasksLocalDataSource,TasksRemoteDataSource,TasksRepository以及它们实现的接口,TasksDataSource。在这个Todo-mvpSample中,同样适用Contract作为接口首先约定了View和Presenter相关的方法,然后通过实现接口在进一步处理。非常喜欢这样的代码写法,先写好接口,代码查看起来非常清晰,也能明确需要处理的业务功能,方便修改。

把TasksDataSource贴出来给大家感受一下

/**
 * Main entry point for accessing tasks data.
 * <p>
 * For simplicity, only getTasks() and getTask() have callbacks. Consider adding callbacks to other
 * methods to inform the user of network/database errors or successful operations.
 * For example, when a new task is created, it's synchronously stored in cache but usually every
 * operation on database or network should be executed in a different thread.
 */
public interface TasksDataSource {

    interface LoadTasksCallback {

        void onTasksLoaded(List<Task> tasks);

        void onDataNotAvailable();
    }

    interface GetTaskCallback {

        void onTaskLoaded(Task task);

        void onDataNotAvailable();
    }

    void getTasks(@NonNull LoadTasksCallback callback);

    void getTask(@NonNull String taskId, @NonNull GetTaskCallback callback);

    void saveTask(@NonNull Task task);

    void completeTask(@NonNull Task task);

    void completeTask(@NonNull String taskId);

    void activateTask(@NonNull Task task);

    void activateTask(@NonNull String taskId);

    void clearCompletedTasks();

    void refreshTasks();

    void deleteAllTasks();

    void deleteTask(@NonNull String taskId);
}

带着注释一起,是不是非常简单明了。直接把项目中需要用到的所有数据操作都直接通过接口定义了出来。还考虑到数据请求的问题连回调都提前设计好了。

Local和Remote分别通过一个类实现这个接口。两边持有完全一样的方法,只是在实现细节上区分开来,local通过Sqlite来操作本地数据,remote则通过Http来获取网络数据(这个Sample中通过Handle模拟远程数据)。

如果有需求新的方法,在接口中加上,两边再implement Methods就可以了,非常方便。

Repository同样实现了这个接口,但是此时它已经不负责数据的操作了,作为与外界交互的直接部分,通过持有Local和Remote,负责提供从两者间挑选出来的数据,以及同时操作两者进行数据更新保存删除等操作。是的Repository其实就是一层外壳,内部操作Local和Remote,并进一步的业务处理。

用Repository中的getTask()方法举例。

  /**
     * Gets tasks from local data source (sqlite) unless the table is new or empty. In that case it
     * uses the network data source. This is done to simplify the sample.
     * <p>
     * Note: {@link GetTaskCallback#onDataNotAvailable()} is fired if both data sources fail to
     * get the data.
     */
    @Override
    public void getTask(@NonNull final String taskId, @NonNull final GetTaskCallback callback) {
        checkNotNull(taskId);
        checkNotNull(callback);

        Task cachedTask = getTaskWithId(taskId);

        // Respond immediately with cache if available
        if (cachedTask != null) {
            callback.onTaskLoaded(cachedTask);
            return;
        }

        // Load from server/persisted if needed.

        // Is the task in the local data source? If not, query the network.
        mTasksLocalDataSource.getTask(taskId, new GetTaskCallback() {
            @Override
            public void onTaskLoaded(Task task) {
                // Do in memory cache update to keep the app UI up to date
                if (mCachedTasks == null) {
                    mCachedTasks = new LinkedHashMap<>();
                }
                mCachedTasks.put(task.getId(), task);
                callback.onTaskLoaded(task);
            }

            @Override
            public void onDataNotAvailable() {
                mTasksRemoteDataSource.getTask(taskId, new GetTaskCallback() {
                    @Override
                    public void onTaskLoaded(Task task) {
                        // Do in memory cache update to keep the app UI up to date
                        if (mCachedTasks == null) {
                            mCachedTasks = new LinkedHashMap<>();
                        }
                        mCachedTasks.put(task.getId(), task);
                        callback.onTaskLoaded(task);
                    }

                    @Override
                    public void onDataNotAvailable() {
                        callback.onDataNotAvailable();
                    }
                });
            }
        });
    }

   private void getTasksFromRemoteDataSource(@NonNull final LoadTasksCallback callback) {
        mTasksRemoteDataSource.getTasks(new LoadTasksCallback() {
            @Override
            public void onTasksLoaded(List<Task> tasks) {
                refreshCache(tasks);
                refreshLocalDataSource(tasks);
                callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
            }

            @Override
            public void onDataNotAvailable() {
                callback.onDataNotAvailable();
            }
        });
    }

注释写的非常清楚,整体逻辑就是先从缓存中获取,如果有,直接返回结果,这是最高效的方式。其次,没有就从本地数据库检索,本地数据不可用的情况下,最后才通过网络获取数据。得到数据结果同时更新缓存以及本地数据。

整个流程相当高效严谨,节约了不必要的操作,操作数据的同时也充分考虑到性能的优化。这些在平常的开发中往往是容易忽略的地方。而Repository中的其它方法也大体类似,主要是优化细节处理,完善数据操作的逻辑。从而更优的与外层进行数据交互。

Google Sample中Model层的调用

作为一个MVP结构中的Model层。依照结构图,Repository被Presenter持有,Presenter同时也持有View层。即Precenter通过Repository进行数据操作,再依据数据操作的结果通知View层进行页面刷新

这里,大家应该就对Model层中Repository的主要作用以及MVP各层级的关系比较明确了,也就不详细赘述了。有兴趣的可以自己下载Google设计模式相关demo进行学习。

当然,学习是为了更好的运用,到了自己的项目中,也不必生搬硬套,没有最好只有最合适的。希望大家都能有所收获~

Google设计模式系列

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容