專案的改造——RemoveButterKnife外掛程式碼的重構

u3shadow發表於2018-12-20

前言

這篇文章記述了我的外掛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);
    }
}
複製程式碼

對比

我們可以從重構前/後的目錄結構來對比重構的效果

重構之前

重構之後

可能會有人問了,重構後感覺複雜了很多,但是從邏輯的維度上來說,一個熟悉設計模式的程式設計師可以很快/方便的閱讀重構後的程式碼,而重構前的程式碼雖然看起來檔案少,但是所有邏輯都在一個檔案中,往往會讓人無法閱讀/理解

相關文章