当前位置:首页 > 技术文章 > 正文内容

项目的改造——RemoveButterKnife插件代码的重构

u3blog2年前 (2022-10-08)技术文章592

前言

这篇文章记述了我的插件RemoveButterKnife的代码改进过程以及思路,关于插件,各位可以看RemoveButterKnife代码库,关于文章,可以看构思到实现RemoveButterKnife

原因

近期想给原来的插件RemoveButterKnife加入一些新的功能,发现以前的代码没有使用任何的设计模式,全部功能都写在一起,对于新功能的添加来说十分糟糕。趁此机会重构了一下代码,在此记录过程。

具体步骤

插件主要分为三个部分

  1. 主插件入口部分
  2. 代码寻找/处理部分
  3. 代码生成部分

    1. 主插件入口部分

    我们首先看第一部分,主入口部分,这部分内容主要代码如下
    @Override
     public void actionPerformed(AnActionEvent event) {
         try {
         project = event.getData(PlatformDataKeys.PROJECT);
         Editor editor = event.getData(PlatformDataKeys.EDITOR);
         file = PsiUtilBase.getPsiFileInEditor(editor, project);
         mFactory = JavaPsiFacade.getElementFactory(project);
         mClass = getTargetClass(editor,file);
         Document document = editor.getDocument(); //以上都是从上下文中获取的辅助对象,具体可以查阅idea plugin文档
         new DeleteAction(project,file,document,mClass).execute();//执行删除操作
         }catch (Exception e){
             e.printStackTrace();
         }
     }
    这部分主要是获取一些需要处理的上下文变量以及下发操作给删除操作,不需要进行处理

    2. 代码寻找/处理部分

    第二部分,也是我们的主要逻辑所在的部分,主要代码逻辑如下
    1.寻找import相关代码,并把行号存入列表
    2.寻找Api调用代码,存入行号
    3.寻找bind相关代码,存入行号,分离id和name以及type,分别存入对应集合
    4.删除上述生成的行号集合对应代码
    5.将生成findview的指令下发给代码生成类
    通过上述逻辑,我们可以看到,1-3步是逻辑不相关部分,没有前后顺序,也没有相互依赖。
    那么,我们就可以通过责任链的模式来对1-3步进行拆分。

    首先,我们创建一个BaseChain作为基类

    BaseChain主要分为三个部分
    1.成员部分
    2.处理逻辑部分
    3.设置子链部分
    代码如下
public abstract class BaseChain {
   protected BaseChain next;
   protected String[] currentDoc;
   protected List<Integer> deleteLineNumbers;
   protected Map<String,String> nameAndIdMap;//第一部分,声明成员
   public void setNext(BaseChain next){
      this.next = next;
   }//设置下一步
    final public void handle(String[] currentDoc,List deleteLineNumbers,Map nameAndIdMap){
        this.deleteLineNumbers = deleteLineNumbers;
        this.nameAndIdMap = nameAndIdMap;
        this.currentDoc = currentDoc;
        process();
        dispatcher();
    }//内部处理逻辑,无法被子类修改
    abstract public void process();//子类需要实现的处理部分
    private void dispatcher(){
        if(next != null) {
            next.handle(currentDoc, deleteLineNumbers, nameAndIdMap);
        }
    }//转发逻辑
}

然后继续创建子Chain类

1.寻找import相关代码,并把行号存入列表
2.寻找Api调用代码,存入行号
3.寻找bind相关代码,存入行号,分离id和name以及type,分别存入对应集合
我们这里拿寻找import相关代码,并把行号存入列表来举例

public class DetectImportChain extends BaseChain{

    public static final String IMPORT_BUTTERKNIFE_BIND = "import butterknife.Bind;";
    public static final String IMPORT_BUTTERKNIFE_INJECT_VIEW = "import butterknife.InjectView;";
    public static final String IMPORT_BUTTERKNIFE_BUTTER_KNIFE = "import butterknife.ButterKnife;";
    public static final String IMPORT_BUTTERKNIFE_BIND_VIEW = "import butterknife.BindView;";//定义了我们需要寻找的语句

    @Override
    public void process() {
        for (int i = 0;i < currentDoc.length;i++){
            if (currentDoc[i].equals(IMPORT_BUTTERKNIFE_BIND)||currentDoc[i].equals(IMPORT_BUTTERKNIFE_BIND_VIEW)||currentDoc[i].equals(IMPORT_BUTTERKNIFE_BUTTER_KNIFE)||currentDoc[i].equals(IMPORT_BUTTERKNIFE_INJECT_VIEW)){
                deleteLineNumbers.add(i);
            }
        }
    }//进行处理
}

有了对应的子类,我们还需要加上junit测试,例如

@Test
    public void test_with_api_use() {
        currentDoc[0] = "NotUseApi();";
        currentDoc[1] = "ButterKnife.useApi();";
        chain.handle(currentDoc,deleteLineNumbers,nameAndIdMap);
        int expect = 1;
        int result = deleteLineNumbers.size();
        assertEquals(expect,result);
    }

这时候我们发现,在这几个子类的测试中,每次都需要初始化一些集合,每个都写十分麻烦,于是我们将其抽出来成为基类,代码如下

class BaseTest {
   protected Map<String,String> nameAndIdMap;
   protected Map<Integer,String> typeAndNameMap;
   protected String[] currentDoc;
   protected List<Integer> deleteLineNumbers;
   @Before
   public void init(){
       nameAndIdMap = new LinkedHashMap<>();
       typeAndNameMap = new LinkedHashMap<>();
       deleteLineNumbers = new ArrayList<>();
       currentDoc = new String[10];
   }
}

这样,我们的测试类直接继承这个基类就可以省下一些代码量了。

删除对应行代码

此部分主要是调用idea的api进行处理,所以我们这里不做过多修改,把方法保留在action里即可。

3生成findViewByid部分

生成代码的逻辑是寻找到文本的特定位置然后依据上述找到的id,name等,进行语句的插入
这一部分前期只负责生成findViewById语句,所以做成单个工具类没有问题。
但是随着项目的扩展,我们还会生成更多种类的代码,例如onclick对应的代码序列等,这时我们就需要对其进行重构。

分析行为

该部分的主要操作是寻找代码指定部分,并使用信息生成代码

1.拆分行为

我们可以拆分为两个步骤
1.寻找特定部分
2.按照分类生成代码
生成代码部分可以分为基础行为和特定行为,基础行为是指生成代码的api调用,特定行为是指生成的代码根据种类不同而不同

2.拆分方案

根据上述分析,我们可以使用策略模式进行优化,每一种生成代码都有对应的策略,我们使用的时候只需要根据类别使用不同的策略类来生成即可
首先,我们建立接口GenCodeStrategy

public interface GenCodeStrategy {
    default void genCode(PsiClass mClass, PsiElementFactory mFactory){
        genFindView(mClass,mFactory);
        genOnClick(mClass,mFactory);
    }
    void genFindView(PsiClass mClass, PsiElementFactory mFactory);//生成findviewbyid代码
    void genOnClick(PsiClass mClass, PsiElementFactory mFactory);//生成onclick代码
}

然后,让我们建立一个Context类,GenCodeContext

public class GenCodeContext {
    private GenCodeStrategy strategy;
    public GenCodeContext(){
    }
    public void setStrategy(GenCodeStrategy strategy){
        this.strategy = strategy;
    }
    public void executeStrategy(PsiClass mClass, PsiElementFactory mFactory){
        strategy.genCode(mClass,mFactory);
    }
}

再来看看我们其中一个策略类,ActivityStrategy

public class ActivityStrategy implements GenCodeStrategy{
    private List<String> code;
    public ActivityStrategy(List<String> code){
        this.code = code;
    }
    @Override
    public void genFindView(PsiClass mClass, PsiElementFactory mFactory) {
         try {
            PsiMethod onCreate = mClass.findMethodsByName("onCreate", false)[0];
            for (PsiStatement statement : onCreate.getBody().getStatements()) {
                // Search for setContentView()
                if (statement.getFirstChild() instanceof PsiMethodCallExpression) {
                    PsiReferenceExpression methodExpression
                            = ((PsiMethodCallExpression) statement.getFirstChild())
                            .getMethodExpression();
                    if (methodExpression.getText().equals("setContentView")) {
                        for (int i = code.size() - 1; i >= 0; i--) {
                            onCreate.getBody().addAfter(mFactory.createStatementFromText(
                                    code.get(i) + "\n", mClass), statement);
                        }
                        break;
                    }
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    @Override
    public void genOnClick(PsiClass mClass, PsiElementFactory mFactory) {

    }
}

最后,我们要在原来直接写代码生成的文件FindViewByIdWriter中使用我们的策略模式

public class FindViewByIdWriter extends  WriteCommandAction.Simple {
    PsiClass mClass;
    private PsiElementFactory mFactory;
    List<String> code;
    Project mProject;
    public FindViewByIdWriter(Project project, PsiFile file, PsiClass psiClass, List<String> code, PsiElementFactory mFactory) {
        super(project, file);
        mClass = psiClass;
        this.code = code;
        this.mFactory = mFactory;
        mProject = project;
    }

    @Override
    protected void run(){
            GenCodeContext codeContext = new GenCodeContext();
            codeContext.setStrategy(new ActivityStrategy(code));
            codeContext.executeStrategy(mClass,mFactory);
            codeContext.setStrategy(new FragmentStrategy(code));
            codeContext.executeStrategy(mClass,mFactory);
    }
}

对比

我们可以从重构前/后的目录结构来对比重构的效果

重构之前
undefined

重构之后
undefined

可能会有人问了,重构后感觉复杂了很多,但是从逻辑的维度上来说,一个熟悉设计模式的程序员可以很快/方便的阅读重构后的代码,而重构前的代码虽然看起来文件少,但是所有逻辑都在一个文件中,往往会让人无法阅读/理解

扫描二维码推送至手机访问。

版权声明:本文由u3blog发布,如需转载请注明出处。

本文链接:https://u3blog.xyz/?id=692

分享给朋友:

“项目的改造——RemoveButterKnife插件代码的重构” 的相关文章

开源数据库Postgresql安装/卸载总结

What Postgresql一个开源数据库,类似mysql,由于mysql被收购了,这个数据库正被越来越多的使用 怎么安装?安装非常简单,但是安装过后的初始化还是有点麻烦,具体可以看这篇文章为什么要卸载?安装好之后,如果你很倒霉的话,会遇到postgresql服务怎么都启动不了,输入psql指令显...

AndroidStudio插件开发——RemoveButterKnife从构思到实现

AndroidStudio插件开发——RemoveButterKnife从构思到实现

ReomveButterKnife插件这是一个用于移除代码中对ButterKnife使用的AS插件,接下来我们将从头开始讲讲AS插件开发和这个插件的开发过程地址是<a href="https://github.com/u3shadow/RemoveButterKnife"...

项目的升级-给RemoveButterKnife插件增加新功能

项目的升级-给RemoveButterKnife插件增加新功能

前言经过项目的初步编写和进一步改造,RemoveButterKnife插件终于也有模有样了,但是,功能上仅仅支持Activity/Fragment的BindView注解。 关于编写和优化的过程可以看下面两篇文章项目构造RemoveButterKnife 项目改进-重构RemoveButterKn...

Android测试体系-在MVVM架构中如何测试Model层与ViewModel层

背景此文章是对于google code lab中《Introduction to Test Double and Dependence injection》 与 《Testing Basics》的总结,本篇主要讲述如何在mvvm架构的android项目中对Model层以及ViewModel层进行测试...

使用Databinding为Recyclerview使用同一个ViewHolder加载不同Item

提示:在阅读本篇文章前,你最好对android databinding有一定了解,本文使用的代码均为kotlin,但是不用担心,都很简单 最近在写项目的时候使用了databinding技术,突发奇想,databinding是不是也能应用于recyclerview中,让加载多个不同的item更简单呢...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。