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

前言

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

缘由

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

具体步骤

插件主要分为三个部分程序员

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

1. 主插件入口部分

咱们首先看第一部分,主入口部分,这部份内容主要代码以下github

@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步进行拆分。api

首先,咱们建立一个BaseChain做为基类

BaseChain主要分为三个部分 1.成员部分 2.处理逻辑部分 3.设置子链部分 代码以下bash

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相关代码,并把行号存入列表来举例ide

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);
    }
}
复制代码

对比

咱们能够从重构前/后的目录结构来对比重构的效果

重构以前

重构以后

可能会有人问了,重构后感受复杂了不少,可是从逻辑的维度上来讲,一个熟悉设计模式的程序员能够很快/方便的阅读重构后的代码,而重构前的代码虽然看起来文件少,可是全部逻辑都在一个文件中,每每会让人没法阅读/理解

相关文章
相关标签/搜索