阅读 1860

MVP那些事儿(7)……Repository设计分析

目录

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层和它的实现:

我们通过一个类图来表示:

知识点:装饰设计模式,赋能与递归

很显然,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的设计增加几个场景。

关注下面的标签,发现更多相似文章
评论