安卓开发:抽象工厂模式+Android AutoService自动创建不同类型的view

361 阅读10分钟

最近在准备秋招,作为有过一年安卓开发经验的菜鸟,没想到秋招一败涂地。痛定思痛,决定重学安卓,因此开一个博客,记录学习的经验和相关总结。

这一篇文章,我将介绍如何通过抽象工厂+Android AutoService实现自动创建不同类型的view。注意:此方法会一次加载多个实例到内存,对内存有所影响。如果您的业务场景不能保证使用所有的实例,请谨慎选择,对于按需加载,也会在文章最后提及

(1) 需求定义

首先,我们明确需求。话说,还在杭州打工的时候,有一天老板和产品对我说,袁少,能不能做一个需求,我们的首页HeadView位支持不同的ViewType,根据不同的ViewType放不同的View。 比如:

ViewType=1,放小卡片View,

ViewType=2,放轮播图View,

ViewType=3,放广告海报View。

我一听:害,这种弱智需求能难得倒我?啪啪啪,我就在MainActivity把代码写出来了:

private View createViewByType(int type) {
    View createdView = null;
    switch (type) {
        case 1:
            createdView = myContext.getLayoutInflater().inflate(R.layout.view_1, null);
        case 2:
            createdView = myContext.getLayoutInflater().inflate(R.layout.view_2, null);
        case 3:
            createdView = myContext.getLayoutInflater().inflate(R.layout.view_4, null);
    }
    return createdView;
}

别告诉什么单一职责,什么封装,可扩展,我面试的时候背的设计模式原则什么的都是狗屁,主打的就是一个简单直接。

562a74b1962e45e3776cd00f24c97097.gif

可是后面真把哥们搞懵逼了,别的先不说,就那个 “type” 可太让人闹心了。服务端那边就只给传过来 1、2、3、4..... N个数字,每个数字对应什么type经常换来换去。每个数字代表的具体含义是什么,我是真记不住呀,每次都得翻半天文档才能找到对应的创建逻辑,维护起来别提多麻烦了。

而且,随着业务的发展,“type” 的种类越来越多,每一种 “type” 对应的视图创建好之后,还有初始化、网络请求以及数据绑定等一大堆业务逻辑要往里塞。这下可好,那个 switch - case 语句里的代码量蹭蹭往上涨,看着那一大片代码,真是让人头皮都发麻!

所以,已老实,求放过,着手重构,家人们。

(2) 加入抽象工厂模式

回想我学过的设计模式,哎,这有个抽象工厂模式,非常适合这里的业务逻辑,天才呐!什么是抽象工厂模式?网上的介绍说实话没用之前我也是背的,理解起来比较困难。我下面说一下我个人的理解。

首先是抽象工厂模式的组成

  • 抽象工厂(Abstract Factory) :它是一个接口或者抽象类,声明了创建一系列相关产品对象的抽象方法。这个抽象方法并不负责具体的产品创建细节,只是定义了创建的规范。例如,在一个游戏开发中,抽象工厂可以声明创建游戏角色、游戏场景、游戏道具等相关产品的抽象方法。
  • 具体工厂(Concrete Factory) :它实现了抽象工厂接口或者继承自抽象工厂抽象类,负责具体的产品对象创建。每一个具体工厂对应一种具体的产品系列组合。比如,对于一款有不同风格(科幻风格、魔幻风格)的游戏,就可以有科幻风格的具体工厂负责创建科幻风格的游戏角色、科幻风格的游戏场景等,魔幻风格的具体工厂则创建相应魔幻风格的产品。
  • 抽象产品(Abstract Product) :它也是接口或者抽象类,定义了产品对象的公共属性和方法,是具体产品类需要遵循的抽象规范。以游戏角色为例,抽象产品角色类可能定义了角色的基本行为,像移动、攻击等抽象方法。
  • 具体产品(Concrete Product) :实现了抽象产品接口或者继承自抽象产品抽象类,代表了具体的产品对象,有着各自具体的实现细节。比如科幻风格的游戏角色类会具体实现移动、攻击等行为,并且带有科幻风格的特效展示,这就是具体产品。

抽象工厂模式的核心要点无疑聚焦在 “抽象” 这一关键特性之上,想必大家都清楚,在 Java 编程的世界里,抽象可是实现多态这一重要特性的关键机制。多态有什么用?有用,可太有用了,下面会看到的

好,这里我们的产品就是要创建的View,简单起见,不做进一步的定义。

我们直接开始用抽象工厂模式重构,先定义一个抽象创建工厂和其N个具体实现类

抽象工厂

public interface CreateViewFactory {
    public View createView(LayoutInflater inflater);
}

具体工厂1

public class CreateView1Factory implements CreateViewFactory{
    @Override
    public View createView(LayoutInflater inflater) {
        View view= inflater.inflate(R.layout.view_1,null);
        //todo 其他初始化业务逻辑
        return view;
    }
}

如果你认真看了,宝,你认真看了对吗? 你看,这里我的抽象工厂被定义为一个接口而不是抽象类,你不好奇吗?为什么这样子,那,玄妙在后面,且听我慢慢道来。

好了我有抽象工厂和具体工厂了,再定义一个创建helper:

public class CreateHelper {
    // 直接创建实例,在类加载时就完成实例化
    private static final CreateHelper instance = new CreateHelper();

    // 私有构造函数,防止外部通过new关键字创建实例
    private CreateHelper() {}

    // 提供一个公共的静态方法获取实例
    public static CreateHelper getInstance() {
        return instance;
    }
    //根据type创建view
    public View createViewByType(int type, LayoutInflater inflater){
        View view;
        switch (type){
            //根据type 调用不同的创建工厂创建view
            case 1:
                view=new CreateView1Factory().createView(inflater);
            case 2:
                view=new CreateView2Factory().createView(inflater);
                //.........
        }
        return view;
    }
}

这里的业务逻辑是,将创建业务都封装到Helper类中,在调用Helper类的createViewByType方法 创建出具体的产品工厂,接着创建view。

似乎,好像有点脱裤子放屁,😂,createViewByType方法还不是写一大堆的Switch-Case代码,然后就是把具体创建逻辑封装了呗。也没用抽象工厂接口啊!多态体现在哪里啊!

可恨!还是菜的不行,这里我还是要写一大堆Switch-Case代码啊,再研究,下一步我要拿下它!!!且看:

public class CreateHelper {
    // 直接创建实例,在类加载时就完成实例化
    private static final CreateHelper instance = new CreateHelper();

    //创建工厂们
    private static HashMap<Integer, CreateViewFactory> factorys= new HashMap<>();

    // 私有构造函数,防止外部通过new关键字创建实例
    private CreateHelper() {}

    // 提供一个公共的静态方法获取实例
    public static CreateHelper getInstance() {
        return instance;
    }

    public void registerFactory(int type,CreateViewFactory factory){
        factorys.put(type,factory);
    }
    //根据type创建view
    public View createViewByType(int type, LayoutInflater inflater){
        if(factorys.containsKey(type)){
           return factorys.get(type).createView(inflater);
        }
        return null;
    }
    
}

你看

//创建工厂们
private static HashMap<Integer, CreateViewFactory> factorys= new HashMap<>();

这里我加入了一个 HashMap factorys 这里我们将viewtype作为键,抽象工厂接口作为值的类型,这个factorys用于保存的各个具体的创建工厂。这就是多态了。当我需要createViewByType时,只需要根据type取得对应的创建工厂就行了。

    //根据type创建view
    public View createViewByType(int type, LayoutInflater inflater){
        if(factorys.containsKey(type)){
           return factorys.get(type).createView(inflater);
        }
        return null;
    }

再强调一遍, factorys中值的类型是抽象工厂接口,而抽象工厂接口是可以指向其具体工厂实现类的,因此我们才能通过HashMap去获得具体工厂类。这就是多态的好处,大大减少了代码量。

好了好了,如果你看懂了,此处应该有掌声,说明你和我一样入门了。

ceeb653ely8gt74uqwgztg205q08basb.gif

但是,说但是了,求你继续看吧,还有一个问题,怎么把Type和它对应的具体工厂实例保存的啊? 原来是通过 registerFactory方法。

public void registerFactory(int type,CreateViewFactory factory){
    factorys.put(type,factory);
}

哎,原来通过调用这个registerFactory方法手动去注册到HashMap里,这是不是有点不够优雅!!!!!,主要是我懒啊,有20个type,我的手要去注册20遍, 你看这拙劣的代码:


    CreateHelper.instance.registerFactory(1,new CreateView1Factory());
    CreateHelper.instance.registerFactory(2,new CreateView2Factory());
    CreateHelper.instance.registerFactory(3,new CreateView3Factory());
    CreateHelper.instance.registerFactory(4,new CreateView5Factory());
    
.........

    CreateHelper.instance.registerFactory(20,new CreateView20Factory());

image.png

(3)使用AutoService实现自动注册功能

**哎,要是有人帮我注册就好了,还真有,谷歌推出的 AutoService,什么是AutoService:

AutoService 是谷歌提供的一个组件,使用简单但是功能却是非常强大。AutoService 会自动在 META-INF 文件夹下生成 Processor 配置信息文件,该文件包含实现该服务接口的具体实现类。当外部程序加载这个模块时,可以通过该 jarMETA-INF/services/ 里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。

AutoService 其实是基于 SPI 机制,这样我们可以直接跨模块查找到想要的接口实现类,避免不必要的模块间依赖,降低模块之间的耦合性。

说了一堆,也不太好懂,我还是用我们的业务给您说明白吧,在我这里,此时此刻AutoService 就是一个能通过注解帮助我们自动创建和加载接口实例的工具。(当然它不止这么点功能

怎么个事呢? 首先我们引入AutoService到项目:

implementation 'com.google.auto.service:auto-service-annotations:1.0.1'
annotationProcessor 'com.google.auto.service:auto-service:1.0.1'

然后回到我们的抽象工厂接口,给它加上一个方法:

public interface CreateViewFactory {
    public int getType();
    public View createView(LayoutInflater inflater);
}

注意:这里我们加入了一个getType的抽象方法,在具体工厂实现类里,各个工厂返回它们自身创建的viewtype的值。这里不难理解吧,就是viewtype=1的创建工厂返回1,依次类推:看代码;

@AutoService(CreateViewFactory.class)
public class CreateView1Factory implements CreateViewFactory{
    @Override
    public int getType() {
        return 1;
    }

    @Override
    public View createView(LayoutInflater inflater) {
        View view= inflater.inflate(R.layout.view_1,null);
        //todo 其他初始化业务逻辑
        return view;
    }
}

好!,你看这里的具体工厂类,我给它添加了一个注解 @AutoService(CreateViewFactory.class),并且实现了getType()方法返回1。加了有什么用呢?来来注意看,回到Helper类的构造函数里。

// 私有构造函数,防止外部通过new关键字创建实例
private CreateHelper() {
//加载所有添加了@AutoService(CreateViewFactory.class)注解的实现类的实例
    ServiceLoader<CreateViewFactory> load = ServiceLoader.load(CreateViewFactory.class);
    //循环放入factorys map
    for (CreateViewFactory factory : load) {
        if(!factorys.containsKey(factory.getType())){
            factorys.put(factory.getType(), factory);
        }
    }
}

可以看到,这里我使用了ServiceLoader.load(CreateViewFactory.class); 这个语句,来加载CreateViewFactory接口的实现类的实例,并且循环所有实例,根据其type,存放到map之中。

可见,只要具体工厂类加上了这句注解:

@AutoService(CreateViewFactory.class)

那么系统就能自动的加载该接口的具体实现类并创建实例,并且能够通过ServiceLoader.load方法拿到自动创建的实例,而你,我的朋友,就不用手动注册了。

在这里我总结一下,希望各位佬,能看的更明白一点。

1.首先我们可以通过抽象工厂模式来减少创建不同type的View的模版代码量,并且支持扩展,具体来说,我们将所有View创建工厂存放到一个HashMap中,通过type去取创建工厂进行创建View,这个过程中涉及到抽象工厂模式里抽象带来的多态的好处,不难理解。

2.如何保存每个type和其对应的创建工厂到Hashmap?要么手动注册,一行一行的注册,比较蠢笨。而使用AutoService添加注解的方式,能够自动帮我们加载所有具体工厂类的实例,然后通过一个for循环进行注册,既不需要手动注册,也不会漏注册。 缺点就是有些工厂我们用不到也可能被创建实例了,占用了内存

这个方法的缺点就是一次性创建所有的实例并加载到内存之中,对内存占用有影响。 解决:这个时候就不能使用AutoService,我们应当保存各个工厂类的.Class对象到HashMap,然后然后根据type取出.Class进行反射来创建实例。此外可以使用注解处理器APT技术来搞定手动注册问题(下一篇讲解)