一種業務耦合的分治方案設計

語言: CN / TW / HK

前言

在業務迭代的過程中,避免不了業務程式碼交叉混排的情況,同一個呼叫時機出現各種不同業務或功能的程式碼。我們需要將呼叫點作為能力抽象出來,從而成為擴充套件點,將原本序列交叉的程式碼進行抽離,回到真正屬於它的歸處。本文聚焦於實現一種Android技術框架,以儘可能小的開發成本,實現混排業務分治的能力。

方案設計

設計原則

基於介面實現對通用能力的抽離,進行耦合拆分。面向介面程式設計,通過一定的抽象,保證業務分治的同時,還可以保證擴充套件性。

  1. 開閉原則,基於擴充套件點的設計,方便地進行擴充套件,無需修改原類;

  2. 依賴倒置,面向介面程式設計,不依賴具體的實現類;

  3. 介面隔離,將功能抽象成多個隔離的介面,降低耦合;

設計方案

  1. 有A、B、C三個模組,B和C存在相互依賴、相互呼叫的情況,且在A模組中被交叉呼叫。

  2. 由A抽離擴充套件點能力到 A_Interface,B和C在A中的呼叫,就變成了 A_Interface 的實現類在A中的呼叫了,確保A不依賴B和C的具體實現。

  3. 將B、C各分成兩層,上層 implement 為業務實現層,程式碼封閉不對外暴露。下層 interface 為介面層,會對外部公開,可以被外部依賴,表示對外提供的介面能力。B和C之間通過相互提供的介面能力呼叫。

  4. 分層之後,implement 實現層之間的依賴耦合被去除,只會依賴介面層。

  5. 將介面和實現的對映關係儲存到工具API類中,實現層通過工具API類實現相互呼叫。

Chain框架

框架設計

  • 實現類標註自己實現的介面和名字,生成一種索引關係,將索引儲存到API單例類 ChainBlock 中。

  • ChainBlock 通過 interface.class 和 key 唯一索引到一個實現類上,呼叫實現類的介面方法。

編譯期索引生成

將介面類和例項物件的索引關係,通過手動呼叫的方式註冊到 ChainBlock 的單例中,會變得相當繁瑣。如果編譯時可以掃描到所有註解類,通過註解生成索引關係,這樣在執行時就可以直接使用,大大降低了使用成本。

  1. 有A、B、C三個 module,拆成獨立的庫,分別編譯成aar整合到主工程。

  2. 在每個 module 中,通過 @Chain 標註需要通過介面公開的實現類上,被註解的類會在子庫編譯成aar時,通過 AnnotationProcessor 和 javapoet 在編譯期被索引到 XChainImpl.class 中,每個子庫都會生成對應的XChainImpl.class。

  3. 在主工程編譯時,通過自定義的 plugin,通過 transform、asm、javassist,將子庫所有的索引類XChainImpl.class 插樁到指定的 ChainJoint.class 中,實現對索引類的聚合。

  4. 在執行時, ChainBlock 工具類可以從 ChainJoint 中獲取所有的索引,Amodule 就可以通過 ChainBlock 獲取 Bmodule 中註解了 @Chain 的實現類了。

執行時API實現

  1. 添加註解 @ChainHost、@Chain、@Block,分別對介面、實現類、方法進行註解,加上註解才能成為對外公開的API能力。

  2. 編譯時索引,通過介面類、實現類名字,索引到唯一的實現類上。索引在編譯時建立,不佔用執行時效率。

  3. 執行時呼叫,通過ChainBlock工具類,完成具體實現類的呼叫。

  4. 詳細API還包括,獲取所有的Chain列表,以及通過動態代理的方式呼叫所有實現類的方法,甚至可以通過一個自定義的協議url調起某個公開實現類的方法。

    // 1. 呼叫指定Chain
    ChainBlock.instance().obtainChain(IProvider.class, "A").log("hello");
    ChainBlock.instance().obtainChain(IProvider.class, "BProvider").log("hi");
    // 2. 獲取所有Chain
    List<IProvider.class> list = ChainBlock.instance().getChainList(IProvider.class);
    // 3. 按優先順序呼叫所有Chain,按方法上@Block註解的priority從高到低呼叫
    ChainBlock.instance().priorityListProxy(IProvider.class).log("hello");
    // 4. 通過chainblock自定義協議呼叫
    ChainBlock.instance().runChainBlock(
    "chainblock://iprovider/A/log?text=hello", context);
  5. 編譯時文件輸出,將所有子庫中添加了註解的介面和實現類在編譯時輸出到文件,用於查閱現有的一些能力。

    Generated by @Chain
    # interface com.taobao.idlefish.IProvider
    > 描述介面的作用
    1. A : { annotatedClass:com.taobao.idlefish.AProvider, singleton:false }
    1. BProvider : { annotatedClass:com.taobao.idlefish.BProvider, singleton:false }
    1. CProvider : { annotatedClass:com.taobao.idlefish.CProvider, singleton:false }
  6. 基於 Graphviz工具 的視覺化輸出,更直觀看到ChainBlock的整體鏈路。

遇到的問題

  1. 索引關係覆蓋問題

    • 通過 @Chain 註解新增的 name,有可能會重複,導致可能會覆蓋其他Chain的索引,問題一旦出現,只能在執行時發現。

    • 解決辦法:在編譯時進行干預,在Plugin的transform裡,掃描所有的jar包,通過URLClassLoader載入XXXChainImpl.class的索引類,將索引關係新增到 Map 裡面,檢查覆蓋情況,發現覆蓋直接終止編譯報錯,將問題在編譯時提前暴露出來。

  1. 穩定性問題

    • @Chain 註解的實現類有可能會被刪除,刪除之後obtainChain(IProvider.class, "A").log("hello")方法呼叫會遇到空指標問題。

    • 解決辦法:通過 obtainChain 返回的不是真正的實現類,返回的是動態代理的 Proxy 物件,代理到真正的實現類上,即使實現類為 null 了,呼叫代理類也不會報錯。

  1. 編譯時遇到的 broken jar 問題

    • 在 Gradle 編譯時執行 Plugin 的時候,出現了broken jar 的錯誤

    • 解決辦法:broken jar 出現的原因是讀取了一個未被正常釋放的 jar 包,可以通過./gradlew --stop臨時解決。經過排查之後,發現 URLClassLoader 和 javassist 的 ClassPool 在載入類物件的時候,需要將 jar 包路徑 add 進去,未被正常釋放導致的問題,正確釋放之後解決。

特點總結

  1. 介面下沉,基於介面的依賴解耦;

  2. 低侵入式,註解即可;

  3. 自動元件註冊,無需手動管理;

  4. 自定義協議,支援跨程序呼叫;

  5. 自動報表文件輸出,圖形化展示;

  6. 混淆友好,不需要額外新增混淆規則;

最佳實踐

不建議使用的場景:

  1. 如果程式碼可以直接呼叫到,沒必要使用Chain。

  2. 如果程式碼屬於業務無關的通用能力,可以直接下沉到底層庫,沒必要使用Chain。

建議使用的場景:

  1. Chain更適合業務能力的抽象,相互隔離的業務子庫之間,有相關程式碼需要複用,可以通過Chain實現。

  2. 向外提供擴充套件點,通過介面約束擴充套件點的方法,可以使用Chain拿到所有實現類來批量處理,並且新增實現類的時候對擴充套件點是無感知的。比如JSBridge的呼叫分發,實現類可以通過Chain進行聚合分發。

基於Chain的業務改造

  • 改造前,在 MainActivity 中各種業務呼叫相互穿插耦合,難以維護。

  • 改造後,抽象一個 IWorkflow 介面,將 MainActivity 中的生命週期能力提供出來,各業務方實現這個介面並添加註解,MainActivity 就可以通過 ChainBlock 工具將生命週期分發到各個業務實現上。各個業務模組程式碼可以收斂到一處,進行分治互不影響。

總結

目前Chain的能力在業務拆解過程中發揮了比較重要的作用,實現了各業務程式碼的分治,同時低侵入式的特性也大大降低了改造的成本。Chain可以作為元件化能力的補充,希望可以給大家帶來一些啟發和收穫。

:tangerine:橙子說

閒魚技術聯合大淘寶技術

新春拜年

“虎虎虎”

紙質紅包大派送

掃碼關注”淘系技術“回覆"紅包“即可獲得領取方式

(2月28日18:00截止)