ReverseSuper:优雅的重构,一键生成动态代码,支持生成接口/抽象类

764 阅读6分钟

ReverseSuper

Download
Download

作用

一键生成动态代码,支持生成接口/抽象类

引用

    implementation 'com.licola:reversesuper-annotation:1.2.0'//注解库
    annotationProcessor 'com.licola:reversesuper-compiler:1.2.0'//代码生成工具库

使用

下面展示库的动态生成能力

/**
 * Created by LiCola on 2018/3/15.
 * 使用示例,主要针对已经存在的类,rebuild后就可生成对应的接口类。
 * 避免需要手动编写,针对项目重构,抽象等,加快开发。
 * 其中AccountManager接口类是动态生成,它抽象目标类的public方法
 */
@ReverseImpl
public class AccountManagerImpl implements AccountManager {

  /**
   * 被反向生成抽象方法的 目标方法
   *
   * @param input 输入值
   * @return 固定返回
   */
  @Override
  public String reverseMethod(String input) {
    return "被反向生成抽象方法的 目标方法";
  }

  /**
   * 被反向生成抽象方法的 目标方法-带参数注解
   *
   * @param integer 带注解的输入范围
   * @return 固定返回值
   */
  @Override
  public String reversMethod(@IntRange(from = 0, to = 10) Integer integer) {
    //展示 方法参数注解 反向生成的能力
    return "被反向生成抽象方法的 目标方法-带参数注解";
  }

  @Override
  @StringRes
  public int reversMethod() {
    //展示 返回值注解 反向生成的能力
    return android.R.string.ok;
  }

  private String value = "不会被处理非方法信息 变量";

  private void privateMethod() {
    //不会被反向生成的私有方法
  }

}

@ReverseImpl注解在目标类上,点击Build-Rebuild,就会动态生成对应的接口类。并且最终的生成代码其实和目标类在相同包下(apk打包过程)。

生成的代码

可以看到一键Rebuild动态生成代码,省略了对现有代码的抽象public方法的手动操作,效率飞快,而且还有对方法注解、参数注解的处理。

同样还有@ReverseExtend注解对目标类生成抽象类。使用类似参见Adapter

项目背景

在项目重构时,面对一些之前因为某些原因没有抽象的模块代码,直接实现功能而没有抽象出接口,直接对外暴露实现类。

当需要把实现类抽象成接口,对外暴露接口,从而实现对接口的mock操作或者装饰者模式添加功能时或者其他操作。

抽象实现类成接口是我们的目标,一般少量的代码手动就可以完成,但是面对大量的实现类需要抽象出接口,这个工作量就是巨大的。

面对巨大且简单重复的工作,首先我们考虑的就是机器代替实现自动化,自动生成代码。而且重构是一个动态的过程,还需要一定的灵活性,因为实现类的函数名和参数都可能变化。

项目实现原理

首先定义我们的目标

  • 自动生成代码
  • 还要灵活性,不只一次生成。

这就联想到Java提供注解处理器,在项目编译前期,注解器有机会处理代码。利用这个处理器,我们可以实现对现有目标类的扫描,然后根据扫描得到的方法信息,利用工具生成java文件。

同时注解处理器的处理过程发生在每次项目编译前期,能够提供灵活性,只要修改实现类再一次编译就会生成新的动态代码。

关于注解的命名Reverse反向:

一般的写代码是从接口(上层)->实现类(下层)。

抽象重构时面对实现类(下层),反而生成接口类(高层),所以就是Reverse反向。

说明

下面说明一些细节问题

关于命名规范

参照阿里巴巴Java开发手册-编程规约-命名风格。

14.接口和实现类的命名规则:

【强制】对于 Service 和 DAO 类,基于 SOA 的理念,暴露出来的服务一定是接口,内部的实现类用 Impl 的后缀与接口区别。 正例: CacheServiceImpl 实现 CacheService 接口

【推荐】如果是形容能力的接口名称,取对应的形容词做接口名 ( 通常是–able 的形式)。 正例: AbstractTranslator 实现 Translatable。

这里有两套规则,对应的到@ReverseImpl注解中两个可选项。其中默认实现【强制】的Impl命名风格。

 /**
  * 反向生成高层接口类注解
  * 被标记的类在编译时,会在build目录下的同级包 生成接口类
  */
 @Target(ElementType.TYPE)
 @Retention(RetentionPolicy.CLASS)
 public @interface ReverseImpl {
   /**
    * 被标注类的名称后缀 默认是命名是 Impl
    * 默认规则 如:AccountMangerImpl(标记类)->AccountManager(生成的接口)
    * 也可以根据实际实现类的后缀修改。
    * 严格检查参数:必须是被标记类的后缀。
    */
   String targetSuffix() default "Impl";

   /**
    * 指定 生成接口名称
    * 默认:默认该字段不作用,通过{@link #targetSuffix}裁剪约定后缀的标记类名称生成接口
    * 非空输入:指定生成的接口名称,忽略后缀检查
    * 如:AbstractTranslator->Translatable
    */
   String interfaceName() default "";
 }

6.【强制】抽象命名使用Abstract或Base开头

这里也是两套规则,对应@ReverseExtend注解,其中默认实现Abstract开头命名风格

/**
 * Created by LiCola on 2018/5/24.
 * 反向生成高层抽象类注解
 * 被标记的类在编译时,会在build目录下的同级包 生成抽象类
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface ReverseExtend {


  /**
   * 生成抽象类名的前缀 默认是Abstract
   * 默认规则:Adapter(标记类)->AbstractAdapter(生成的抽象类)
   * 也可以传入参数指定生成的抽象类名的前缀
   * 如:传入Base,作用Adapter(标记类)->BaseAdapter(生成的抽象类)
   */
  String superPrefix() default "Abstract";

  /**
   * 指定 生成的抽象类名称
   * 默认:默认该字段不作用,通过{@link #superName()}直接拼接标记类和前缀生成抽象类
   * 非空输入:指定生成的抽象类名称
   * 如:传入BaseAdapter, MyAdapter(标记类)->BaseAdapter(生成的抽象类)
   */
  String superName() default "";

}

关于生成的java文件

当Rebuild项目时,会在app-build-generated-sourceapt文件下,生成目标类的同名包以及动态生成的高层类。最终在apk打包过程中源码.java文件和apt下的.java文件会合并打包。在我们继承接口时即implements AccountManager会认为是导入同名包的下的代码,不会有import语句。

辅助重构-最终弃用

重构是一个渐进的过程,从最初的实现类反向生成接口类。接口类可能会修改。动态的反向可以带来便利。只要添加/修改实现类方法参数/返回值以及它们的注解,rebuild就会马上生成接口。一次修改(否则就要修改接口类和实现类的方法,两次修改)。

当重构完成或者更高层抽象分离出来(比如动态代理,直接抽象方法内部实现逻辑),我们的高层类最终确定,就不需要build项目时动态生成反向接口类。

每次build动态生成反而可能拖慢了项目的编译时间。这时就可以从动态文件包app-build-generated-sourceapt中复制出接口类放到适合的包。从而弃用@ReverseImpl/@ReverseExtend注解。完成它辅助重构的使命。

项目灵感

源自大名鼎鼎的butterknife对注解处理器的应用。

future

其实按照这个思路,可以解决MVP架构中V/P两层接口方法太多时带来的麻烦。 只要我们的思路清晰而且是以一个人分工负责一个V/P对应模块,可以先写P层业务代码,然后一键生成抽象接口暴露public方法,提供给V层使用。而且每次P层方法新增/修改参数都动态生成,避免两次修改的麻烦。