閒談軟體設計:Plug-in

閱讀時間約 14 分鐘

無限擴充

高中參加資訊社,社團有年度刊物,為了編輯與發行年度刊物,跟著學長學了 PageMaker 和 Photoshop,前者是出版業常用的排版軟體 (現今被 InDesign 取代),後者不用多說,號稱是軟體界的 SK II,什麼照片都能修,手繪功力不好的我,常常玩的是 Photoshop 的多種濾鏡,在還有大補帖的時代,我就收集了許多濾鏡,只要把檔案丟到特定目錄,開啟 Photoshop 就可以多好幾種濾鏡可以用,那時就覺得好神奇,為什麼這樣就可以用了?
後來用 Eclipse 開發軟體時,再次為了其 plug-in 的架構感到好奇,不過 Eclipse 的 plug-in 底層運行架構是極為複雜的 OSGi,所以當初在執行國科會計畫時,試著想用自己設計的簡化版 plug-in 架構將兩個不同子計畫的 UI 整合在一起,計畫成果是成功的,不過後來回頭看架構,其實並不是很好的設計,因為沒有 plug-in 相依管理的設計,所以兩個子計畫的程式在彼此相依的情況下,無法單獨安裝也無法像 Eclipse 在安裝時能將相依的套件一併安裝或移除。

Plug-in pattern

後來自己閒暇之餘寫的看漫畫軟體 Comic Surfer,一開始並沒有 plug-in 架構,但後來想想,自己再怎麼寫,也不可能支援所有格式,所以在想有什麼方法可以讓別人擴充 Comic Surfer 能讀取的格式呢?答案很明顯,就是 plug-in 架構,事實上這已經是常用的一種架構模式 (architecture pattern),如果看 Martin Fowler 的 《Pattern of Enterprise Application Architecture》描述何時使用 plug-in pattern?你會看到:
Use Plugin whenever you have behaviors that require different implementations based on runtime environment. — 《Pattern of Enterprise Application Architecture》
更簡單的說法:
Link classes during configuration rather than compilation. — David Rice and Matt Foemmel
不是在軟體建置之初就決定所有一切的行為,而是執行時根據配置 (configuration) 或執行時的環境 (例如:特定目錄有什麼 plug-in) 決定有哪些行為,話是這麼說,但要實作一個可以載入 plug-in 的軟體 (以下稱作 host application) 倒是有不少事情要考慮。

如何載入

首先是怎麼建立 plug-in 的實體 (instance)?以 Java 為例,有 Reflection 機制可以在執行期間動態地載入與建立物件,但要建立那個物件呢?總不能把特定目錄底下的所有 JAR 檔載入後,建立全部類別的實體吧?畢竟不是所有的物件都是作為 plug-in 使用,因此,設計 host application 時要先決定用什麼機制載入 plug-in,可能的機制有:
  • 透過實作特定的介面:在還沒有 annotation 之前,是可以要求 plug-in 實作特定的介面,該介面可以是一個空介面,目的是讓 host application 可以透過掃描所有類別,看哪個類別有實作指定的介面,若有實作就視為是 plug-in 的主物件。
  • 為 plug-in 物件加上特定annotation:自從 Java 可以自訂 annotation 後,主流就變成以 annotation 提供 metadata 了,所以 host application 可以掃描所有類別,看有沒有特定的 annotation,像 Spring framework 就大量使用這個機制載入元件。
  • 透過文件提供 metadata:雖然說以現在 CPU 的能力,掃描所有類別應該花不上多少時間,但若有指定的規則,可以加速載入的時間,像 Eclipse 的 plugin.xml,裡面提供足夠的資訊讓 Eclipse 決定怎麼載入它,例如 Comic Surfer 就要求 plug-in 提供一個 reader.xml 的檔案,讓 Comic Surfer 知道該建立哪個物件:
不管是哪一種方式,都只是讓 host application 得知該載入哪個類別,但是要用 Reflection 建立物件,還是得知道建構子有哪些參數,最簡單的機制是要求 plug-in 的物件提供預設建構子,也就是零參數的建構子 (zero-argument constructor),讓 host application 簡單地建立物件,但這就會讓 plug-in 失去 constrcutor-injetion,若要讓 plug-in 的建構子有參數,可以參考 Spring framework,用 XML 或 annotation 描述參數的來源。

提供服務

當 plug-in 的實體被載入,host application 怎麼知道該 plug-in 提供什麼服務呢?最單純的做法是由 host application 定義介面,然後由 plug-in 提供實作,以 Comic Surfer 為例,plug-in 能提供多個讀取器,透過 reader.xml 所建立第一個物件,必須實作 ComicBookSuiteReaderFactory,host application 可以呼叫 createReaders() 由 plug-in 自行建立讀取器的實體:
除此之外,Comic Surfer 還定義了許多介面要求讀取器實作,像是 ComicBookSuiteReader 負責讀取整套的漫畫,每套漫畫必須是實作 ComicBookSuite 的實體,一套漫畫中的每本漫畫是 ComicBook 的實體,最後每本漫畫的每一頁面是 Page 的實體,如此,Comic Surfer 根據這些介面知道這個讀取器能讀什麼漫畫 (透過 canRead(URI) 判斷),當漫畫被讀進來後 (透過 read(URI)),可以知道這套漫畫有幾本,每本有幾頁,能讀取每一頁的內容,有了這些,所有換頁、換冊以及快取等機制都能正常運作。
Comic Surfer plug-in 需實作的介面
Comic Surfer 會在程式啟動時檢查自身路徑底下的 readers 目錄是否有 plug-in,有的話就會讀取進來,一般來說這已經很足夠,如果要更進一步可以監控目錄是否有變化,這可以做到在程式運作時就能動態載入 plug-in 不須重新啟動應用程式,若搭配上安裝介面,就能做到安裝完就立即能使用。

參數

回頭看 Photoshop 的濾鏡,每個濾鏡都是一個 plug-in,不過由於每種濾鏡都需要不同類型、數量的參數才能使用,更重要的是提供預覽畫面。為此,host application 可以有幾種做法,第一種是 host application 定義一個函式,例如 preview(Image),host application 將要套用濾鏡效果的原始圖副本 (讓使用者可以取消) 交給 plug-in,由 plug-in 自行建立預覽畫面與設定參數的 UI controls,按下確定後,host application 呼叫 apply(Image) 讓變動生效,這對 host application 較為簡單,因為預覽畫面的責任在 plug-in 身上。
但若為了 host application 整體 UI 的一致性,也可以由 host application 定義大多數參數的 UI controls,然後定義介面讓 plug-in 提供參數,host application 建立預覽畫面與控制項,然後將參數與控制項綁定,plug-in 則藉由事件綁定得知使用者調整了哪些參數,立即更新預覽的效果,這樣的話 host application 責任較大,但對開發 plug-in 的開發者就會比較輕鬆,專注在濾鏡的演算法上。

相容性

定義介面,然後由 plug-in 提供實作,但當 host application 需要 plug-in 提供更多功能時,一般會在介面上新增函式。某些語言,像 Objective C 可以將介面中某些函式宣告為 @optional,也就是說繼承的類別可以不需要提供實作,host application 可以在執行期間檢查是否有時做特定函式,若有才呼叫,若沒有則以預設的行為取代。
但並不是所有的語言都有這樣的設計,像 Java 在載入類別時會檢查類別是否滿足所實作的介面,若驗證失敗就不會載入該類別,因此 host application 在提供 SDK 讓第三方開發 plug-in 時,盡量提供一個抽象類別讓第三方繼承,而不是直接讓第三方實作介面,當要在介面新增函式時,可以在抽象類別中提供預設行為的實作,如此第三方即使不重新編譯,既有的 plug-in 也能繼續使用。不過,這是有預設行為的前提,若沒有的話還是會面臨不相容的問題。[2017/5/28 補充,若當初是使用 Java 8,那介面的 default methods 可以解決擴充引起的相容性問題。]

請求支援

到目前為止,都是由 host application 向 plug-in 請求服務,但 plug-in 有時候也會需要向 host application 請求資源才有辦法提供服務,例如可以向 Eclipse 請求取得目前檔案的 AST (abstract syntax tree),進行語義的分析然後提供額外的服務。為了保護 host application 自身,並不會讓 plug-in 毫無限制取得任何資源或服務,因此,還是會定義一個介面作為 Facade,讓 plug-in 註冊事件、取得資源、操作資源等等,不在 Facade 介面中的函式 plug-in 就無法使用。如何讓 plug-in 取得該介面的實體呢?constructor injection 或 setter injection 都是可以使用的方法,要求 plug-in 提供注入 Facade 的建構子或是提供 setter 讓 host application 在建立 plug-in 後呼叫 setter 注入。
現在,host application 可以呼叫 plug-in 特定介面取得額外的服務,plug-in 也可以透過介面存取 host application 的服務與資源,那 plug-in 之間要交換資訊怎麼辦呢?原則上,plug-in 之間直接互動不是不行,但如此一來,plug-in 之間就變成較強的耦合,若想避免這情況,一個較好的做法是透過 host application 中介,plug-in 向 host application 請求服務,host application 看自身能否提供,若不能提供再詢問是否有其他 plug-in 提供該服務。
不過,不管是 plug-in 直接呼動或是透過 host application 中介,最後都會在交換的資料結構產生耦合,畢竟,直接存取資料物件會方便很多,特別是交換大量資料更是如此。若資料量不大,是可以轉成與特定物件格式無關的文字或文字檔,例如以 URL 的 query parameters 傳遞參數,以 JSON 格式回傳資料等等,但資料的解析還是省不掉 (至少要知道參數名稱),因此由 host application 定義可能交換的資料格式或使用語言內建的資料格式也是一種方法,就看資料存取的方便性與 plug-in 之間獨立性的平衡點要抓在哪裡。

實例

設計 plug-in 架構還有很多東西可以考慮,例如怎麼安裝、移除、檢查相依性等等,但如果只是簡單的 plug-in 架構,上述的內容應該能滿足大多數的需求。最後,若以 plug-in 架構來看 Android,就可以知道什麼是 Intent,知道為什麼 Activity 必須要有 zero-argument constructor,還有為什麼要在 AndroidManifest.xml 宣告那麼多東西了?
簡單說,每個安裝到 Android 中的 apk 都可以視為是一個 plug-in,AndroidManifest.xml 告訴 Android (也就是 host application) 啟動這個 plug-in 要建立哪個 activity,該 plug-in 提供什麼 <service> 和 <provider> (資料)。而 plug-in 之間則是以 Android 中介,透過 Intent 告訴 Android 想要什麼服務或資料,Android 會根據每個 plug-in 的 <intent-filter> 描述決定啟動哪個 plug-in 來滿足請求,Intent 可以夾帶參數給另一個 plug-in,並在完成後回傳資料。

結語

雖說用 Android 的例子做結尾,但是我個人還是不喜歡 Activity 的建構子不能有參數,也不能自己建立 Activity 的物件,造成這的最主要原因是 Activity 身兼太多責任了,既是 plug-in,也是 MVP 中的 presenter (是的,不是 MVC),實在不是一個 high cohesion 原則下該有的設計。

目前閒談軟體設計系列,每一篇大概都需要 1x 小時,包含構思、收集資料、撰寫與修稿,每個禮拜一篇已經是極限了,還好有龐大的 architecture pattern 素材,題材不是大問題,但要有自己的風格,內容還是盡量以實作及自身的經驗為主,撰寫起來還是要想蠻久的。話雖如此,有任何想知道的題材,可以留言告知,若本來就有打算寫相關的題材,可以盡量將該題材的 priority 往前移。
回頭搬這篇文章到方格子,有點感慨,當初應該繼續開發 ComicSurfer 的,雖然知道後續版權和取得授權都是很難的事,但至少是自己喜歡的東西,創業圈最不缺的就是點子,缺的是實現的能力與毅力,對於有想創業的人,共勉之。
為什麼會看到廣告
53會員
104內容數
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
留言0
查看全部
發表第一個留言支持創作者!
Spirit的沙龍 的其他內容
基本上就是這樣,例外處理是可以在軟體架構設計時就考慮進去,或者說,在軟體架構設計時就該考慮進去,制定方針讓團隊有一個原則可以遵循,透過設計讓例外的處理較容易與一致,最終讓軟體的品質可以更好。
設計時的考量主要有:(1) App 是 Internet App,在考量 UI 體驗和網路頻寬的消耗,多數資料需以某種形式儲存部分資料在行動裝置上;(2) 因此會需要同步伺服器端和行動裝置端之間資料狀態;(3) 但行動裝置網路的穩定性不如一般網路可靠,要有足夠的自動化測試驗證正常的流程與異常的流程。
程式開發有趣的地方,同樣的目標,不同的團隊會因不同的因素做出不同的設計抉擇。而這往往也是為什麼一個資深的工程師在開發速度上不一定比較快的原因之一,一個越是資深的工程師,思考的因素會更多,不過,不是考慮得越多就結果就一定越好,有時還會變成 over design 較糟的結果。
唸研究所開始當助教,偶而會有學弟妹問:怎樣寫好程式?老實說,這是個大哉問,連我學開發軟體這麼久,我也只能回答他們:多培養自己釐清問題、拆解問題、解決問題與抽象化的能力。但他們通常只會一臉狐疑看著我,感覺我說的話好抽象。
在履歷中常常看到導入 MVVM,然後問為什麼要導入 MVVM 時,最常聽到的答案是這樣不會有很肥大的 view controller,但如果再問 view controller 是 MVC 的那一個部分,很多人卻回答不出個所以然,所以想聊聊這個很多種說法的 MVC pattern。
為什麼是煮拉麵呢?主題是來自前同事在問我為什麼有人的程式好像常常會歪掉,或是變得難維護,後續的討論中,他用的例子就是拉麵,所以... 今天就用程式來煮拉麵吧!
基本上就是這樣,例外處理是可以在軟體架構設計時就考慮進去,或者說,在軟體架構設計時就該考慮進去,制定方針讓團隊有一個原則可以遵循,透過設計讓例外的處理較容易與一致,最終讓軟體的品質可以更好。
設計時的考量主要有:(1) App 是 Internet App,在考量 UI 體驗和網路頻寬的消耗,多數資料需以某種形式儲存部分資料在行動裝置上;(2) 因此會需要同步伺服器端和行動裝置端之間資料狀態;(3) 但行動裝置網路的穩定性不如一般網路可靠,要有足夠的自動化測試驗證正常的流程與異常的流程。
程式開發有趣的地方,同樣的目標,不同的團隊會因不同的因素做出不同的設計抉擇。而這往往也是為什麼一個資深的工程師在開發速度上不一定比較快的原因之一,一個越是資深的工程師,思考的因素會更多,不過,不是考慮得越多就結果就一定越好,有時還會變成 over design 較糟的結果。
唸研究所開始當助教,偶而會有學弟妹問:怎樣寫好程式?老實說,這是個大哉問,連我學開發軟體這麼久,我也只能回答他們:多培養自己釐清問題、拆解問題、解決問題與抽象化的能力。但他們通常只會一臉狐疑看著我,感覺我說的話好抽象。
在履歷中常常看到導入 MVVM,然後問為什麼要導入 MVVM 時,最常聽到的答案是這樣不會有很肥大的 view controller,但如果再問 view controller 是 MVC 的那一個部分,很多人卻回答不出個所以然,所以想聊聊這個很多種說法的 MVC pattern。
為什麼是煮拉麵呢?主題是來自前同事在問我為什麼有人的程式好像常常會歪掉,或是變得難維護,後續的討論中,他用的例子就是拉麵,所以... 今天就用程式來煮拉麵吧!
你可能也想看
Google News 追蹤
Thumbnail
接下來第二部分我們持續討論美國總統大選如何佈局, 以及選前一週到年底的操作策略建議 分析兩位候選人政策利多/ 利空的板塊和股票
Thumbnail
🤔為什麼團長的能力是死亡筆記本? 🤔為什麼像是死亡筆記本呢? 🤨作者巧思-讓妮翁死亡合理的幾個伏筆
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
塔西佗陷阱(Tacitus Trap),一個得名於古羅馬歷史學家塔西佗的政治學理論,意指倘若公權力失去其公信力,無論如何發言或是處事,社會均將給予其負面評價。 當然信著恆信、不信者恆不信,這就是真實的人生。
Thumbnail
關於片名   台灣片名《花漾女子》,原文片名《Promising Young Woman》,台灣譯名將時間定格在悲劇發生前,而原文片名則進一步帶我們看見另一個可能性結果
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作
Thumbnail
那天我問隊友,怎樣才算是一部小說呢?按字數計算嗎? 他說:「故事內起承轉合都有,就算」 所以我大膽地按著他的標準,將自己寫過的故事,粗略整理出一個明細。
纏中說禪,本名李彪,專欄筆名木子,其人是中國股市比較早期的操盤手,所以他比較熟悉a股的市場情況。他以“纏中說禪”為筆名,從2002年開始寫博客,直到2008年癌症病重停更,期間寫下了不少文章。而博客文章中最為著名的就是他的“教你炒股票”系列文章,他在這個系列裡講到的炒股理論和方法被粉絲稱為“纏論
Thumbnail
婚姻是人生大事,對溥儀尤其如此,因為如果皇帝大婚,就代表溥儀可以脫離眾多便宜老媽的束縛而得以親政。 但詭異的是,這個可以讓他脫離便宜老媽掌控的婚姻,卻還是要由便宜老媽進行主導並且居中角力......
Thumbnail
上次我提到:溥儀就是個死小孩。其實這不能全怪溥儀,而要怪詭異的宮廷教育及生活制度......
Thumbnail
近期電影「末代皇帝」重新修復上映。 為了推坑這部經典之作,本人決定以溥儀本身的自傳《我的前半生》為主要基底,和大家談一些電影中礙於篇幅或是藝術改編,而不容易察覺或是沒有呈現的真實歷史。
Thumbnail
接下來第二部分我們持續討論美國總統大選如何佈局, 以及選前一週到年底的操作策略建議 分析兩位候選人政策利多/ 利空的板塊和股票
Thumbnail
🤔為什麼團長的能力是死亡筆記本? 🤔為什麼像是死亡筆記本呢? 🤨作者巧思-讓妮翁死亡合理的幾個伏筆
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
塔西佗陷阱(Tacitus Trap),一個得名於古羅馬歷史學家塔西佗的政治學理論,意指倘若公權力失去其公信力,無論如何發言或是處事,社會均將給予其負面評價。 當然信著恆信、不信者恆不信,這就是真實的人生。
Thumbnail
關於片名   台灣片名《花漾女子》,原文片名《Promising Young Woman》,台灣譯名將時間定格在悲劇發生前,而原文片名則進一步帶我們看見另一個可能性結果
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作
Thumbnail
那天我問隊友,怎樣才算是一部小說呢?按字數計算嗎? 他說:「故事內起承轉合都有,就算」 所以我大膽地按著他的標準,將自己寫過的故事,粗略整理出一個明細。
纏中說禪,本名李彪,專欄筆名木子,其人是中國股市比較早期的操盤手,所以他比較熟悉a股的市場情況。他以“纏中說禪”為筆名,從2002年開始寫博客,直到2008年癌症病重停更,期間寫下了不少文章。而博客文章中最為著名的就是他的“教你炒股票”系列文章,他在這個系列裡講到的炒股理論和方法被粉絲稱為“纏論
Thumbnail
婚姻是人生大事,對溥儀尤其如此,因為如果皇帝大婚,就代表溥儀可以脫離眾多便宜老媽的束縛而得以親政。 但詭異的是,這個可以讓他脫離便宜老媽掌控的婚姻,卻還是要由便宜老媽進行主導並且居中角力......
Thumbnail
上次我提到:溥儀就是個死小孩。其實這不能全怪溥儀,而要怪詭異的宮廷教育及生活制度......
Thumbnail
近期電影「末代皇帝」重新修復上映。 為了推坑這部經典之作,本人決定以溥儀本身的自傳《我的前半生》為主要基底,和大家談一些電影中礙於篇幅或是藝術改編,而不容易察覺或是沒有呈現的真實歷史。