5

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

 3 years ago
source link: http://www.u3coding.com/2018/12/20/rebuild-removebutterknife/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

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

1. 主插件入口部分

我们首先看第一部分,主入口部分,这部分内容主要代码如下

   @Override
    public void actionPerformed(AnActionEvent event) {
        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) {
            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);

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

重构之后

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK