隨筆:組件化的一些思考

筆者所在部門都一個很大的SDK,被公司眾多部門所使用,筆者到來之前,這個SDK采取的是原始的開發(fā)方式,隨著時間的遷移,已經(jīng)變得非常大,大概一千多個OC文件,C++的沒數(shù)過,功能也很豐富,但是對于某些部門,對包大小有非常嚴(yán)格的限制,某些不需要的功能需要減包,對于一個高度耦合在一起的SDK來說,幾乎只能通過預(yù)編譯宏來減少文件的引用,這樣做造成一個git倉庫上維護(hù)著多個分支,每次合并都是噩夢般地存在,而且由于功能模塊很多,對于每一個功能模塊都需要有一個預(yù)編譯宏,這樣帶來一個問題,如果宏的組合發(fā)生錯誤,那么這個極有可能帶來嚴(yán)重BUG。因為筆者面試的時候,和面試官除了常規(guī)技術(shù)問題,談得最多的是架構(gòu)設(shè)計,所以當(dāng)筆者進(jìn)入公司之后,總監(jiān)以及組長都希望我能夠解決掉現(xiàn)在的問題,至少說能夠部分解決,特別是大ZT計劃發(fā)布之后,這件事被真正意義上的開始實施。筆者這篇文章是一篇隨筆,是希望為遇到同樣的事情的開發(fā)者提供一些思路,由于保密的原因,有很多東西沒有辦法說清楚,忘見諒,勿噴。

筆者的任務(wù)在初期是比較簡單和明確的,要求對于某些模塊能夠可選集成,使用cocoaPod的時候,是否集成某個模塊,不能改動一句代碼,有點像友盟的社會組件集成方式,但是比那個應(yīng)該要復(fù)雜一些,后面會細(xì)說原因。

看到這里,有經(jīng)驗的朋友第一個想到的就是組件化,組件化已經(jīng)不是什么稀罕事兒了,業(yè)內(nèi)有非常成熟的URL注冊方案,Mediator方案以及Protocol注冊方案。但是這些成熟的方案其實都是針對App的,App的組件化其實是比較簡單的,因為App有可視化的邏輯幫助我們對整個App進(jìn)行縱向切割和橫向切割,工具與業(yè)務(wù)之間的界限非常明確,業(yè)務(wù)與業(yè)務(wù)模塊之間的界限也非常明顯,特別是在復(fù)合設(shè)計模式的幫助下,代碼的可復(fù)用性變得很大。筆者認(rèn)為,橫向切割解決的是組件的依賴方向問題,縱向切割是解決組件間的黑盒問題,橫向切割完成之后,位于上層的組件可以依賴下層,而下層不能反向依賴,縱向切割完成之后位于同一橫向的組件不能夠直接通訊,這是差不多有經(jīng)驗的開發(fā)者都能夠想明白的事情。但是筆者所遇到的問題是

第一:一個巨型SDK,在某種意義上本身就屬于工具,其界線劃分變得模糊,筆者在以前的公司集成過融云的SDK,融云的SDK分為兩部分,UI和通訊功能部分,界線非常明顯。但是筆者這邊麻煩的是,沒有UI,而且筆者部門的SDK涉及的是圖像處理領(lǐng)域以及人工智能領(lǐng)域,連像App那樣抽出基礎(chǔ)工具類都變得異常麻煩,這個麻煩點來自于,這個基礎(chǔ)工具類可能從邏輯角度都說不通它算是基礎(chǔ)工具,筆者也是為了避免一個問題,基礎(chǔ)工具類很有可能會變成垃圾堆。

第二:它沒有,并且它沒有辦法使用現(xiàn)在成熟的復(fù)合設(shè)計模式,例如MVC,MVVM等,復(fù)合設(shè)計模式帶來的最大的好處是我們能夠在某種規(guī)范下劃分代碼。如第一個條件所說,我們的SDK沒有UI,M肯定是有的,但是C也變得很模糊,例如筆者舉個例子,渲染鏈讀者們認(rèn)為應(yīng)該屬于什么?人工智能那堆C++代碼,橋接代碼就更不用說了。

第三:渲染鏈中一幀數(shù)據(jù)在A模塊的輸出會影響的B模塊該以何種方式處理這一幀的數(shù)據(jù),而B模塊對本幀數(shù)據(jù)不同處理方式又需要告知A模塊下一幀數(shù)據(jù)該如何處理,這就讓整個渲染鏈的通訊方向變成了雙向的。在傳統(tǒng)開發(fā)模式下,正向通訊采用屬性傳值或者調(diào)用方法的方式通訊,逆向通訊采用通知,代理,Block,因為筆者需要實現(xiàn)可選集成,所以代理和Block不能使用,通知是不可能用的,否則這么多模塊的通訊,會讓通知根本沒法維護(hù)。

第四:再說那三種業(yè)內(nèi)比較成熟的組件化方案,URL注冊方案,我見過某大型App用過,這東西很簡單,通過參數(shù)和需要參數(shù)的對象(大多數(shù)情況下是ViewController)組建一個URL,然后傳給Router去解析URL并完成組件的創(chuàng)建和傳值。這方案非常適合組件非常獨立的情況,但是對于復(fù)雜情況來說,應(yīng)該有同學(xué)也會贊同,復(fù)雜通訊情況下,這種方案會變得很無力,而且創(chuàng)建URL方式進(jìn)行通訊,特別是逆向通訊,非常無力。Mediator方案,我沒用過,但是我看過網(wǎng)上很多關(guān)于這套方案的一些框架封裝或者Demo,這種方案核心就是Mediator的分類,由分類調(diào)起服務(wù),但是這樣的話,我們來思考一個問題,如果一個Mediator的分類對一個組件負(fù)責(zé),而這個組件本身功能比較獨特,那么這個分類寫出來對外暴露的接口也會有一定的獨特性,而該Mediator的分類對模塊的通訊也會變得具有獨特性,會造成Mediator的分類對模塊的強依賴關(guān)系,這不像在App組件化中,通過類名創(chuàng)建類那么簡單。就算解決了這個問題,但是如果不集成某個功能模塊,這個Mediator的分類是否應(yīng)該被移除?如果移除,會造成方法找不到,編譯不過,如果不移除,那么就會存在一大堆Mediator,對于連幾K包增量都要計較的團(tuán)隊,顯然是他們不能接受的,這就不符合我們在計劃之出,對于功能模塊可選集成的要求。Protocol注冊方案是筆者認(rèn)為最接近能夠?qū)崿F(xiàn)筆者要求的方案,但是問題來了,誰來遵循這個Protocol(后面還會細(xì)說)?但是無論哪種組件化方案,似乎我們都需要一個中介者,無論是Router還是Mediator。

但是箭在弦上,不得不發(fā),筆者具體做法如下。

第一步:清理整個SDK,這一點是常規(guī)操作,依據(jù)經(jīng)驗,一個經(jīng)歷了幾年迭代的任何代碼工程,肯定有廢棄代碼,廢棄文件以及無效依賴,由于我們的SDK影響非常大,所以筆者不得不采取人肉方式進(jìn)行清理,這樣清理后也放心,先找廢棄文件,廢棄文件會有一個特征,它的依賴鏈會斷,也就是你順著依賴往上找,會發(fā)現(xiàn)在某個時候斷掉了或者直接根本就沒人依賴它。無效依賴是指某個文件可能import了另外一個文件,但是卻沒有使用到這個文件的任何東西。清理會分幾波,因為隨著清理的進(jìn)行,越來越多廢棄的東西會暴露出來,這樣是為以后做準(zhǔn)備。申明一點,這和App不同的是,App由于是可視化的,哪個界面沒用了,一目了然,隨之的ViewController,View,Model都會被廢棄到。

第二步:橫向切割,先確定引用方向,我根據(jù)我們的數(shù)據(jù)的流向從下至上(注:{}表示是同一橫向?qū)樱┓譃閧AI的C++庫,Base庫(這一層做架構(gòu)把控的一定要嚴(yán)格控制好里面的文件,否則一定會變成垃圾堆,我們的base層只存放基類和線程模型)} -> {中介者庫,AI封裝庫,渲染擴(kuò)展庫(主要是openGLES和metal之間的通訊)} -> {算法庫(老大經(jīng)常說,我們的核心科技啊?。。。?,能力庫(也可以理解為業(yè)務(wù)層類似的東西)} -> {能力組裝庫},注意,這里的數(shù)據(jù)的流向成為了我橫向切割的重要依據(jù),而不再使用App根據(jù)業(yè)務(wù),UI,基礎(chǔ)功能,基礎(chǔ)工具這樣的類似劃分。

第三步:縱向切割,這個沒有什么太多說的,根據(jù)業(yè)務(wù)或者功能劃分邏輯來,剛才分好的橫向?qū)与m然在縱向有了粗略的切割,但是不夠,例如人工智能AI模塊,我們有五六種人工智能能力,而C++庫加上AI模型,是很大的,一個人工智能能力加起來可能就是幾百兆了,所以還要分,但是這里早就有大神提到過,縱向粒度劃分一定要合適,劃分得太粗,相當(dāng)于沒怎么劃分,劃分得太細(xì),給自己找麻煩,同時要結(jié)合收益是否和代價相匹配,除非非常明顯的劃分界限,一般不要分開,舉個簡單例子,可能不太準(zhǔn)確,圖形識別和文字識別界線非常明確,應(yīng)該劃分掉,前景貼紙貼花和前景貼紙貼草,這個界線就不那么明確,所以就不要劃分。

第四步:做到這里的時候,筆者其實還是沒有一個非常完美的中介者方案,但是有一個保底方案,回到我剛才面臨的問題中的第四點,“Protocol注冊方案是筆者認(rèn)為最接近能夠?qū)崿F(xiàn)筆者要求的方案,但是問題來了,誰來遵循這個Protocol(后面還會細(xì)說)”,這里有一個比較好的解決方案是對于一個組件來說,每個組件要有一個其統(tǒng)一封裝類,這個統(tǒng)一封裝類對整個模塊負(fù)責(zé),通過這個統(tǒng)一封裝能掉到這個模塊的所有功能(當(dāng)然有些功能不是直接調(diào)用,而是通過其他類轉(zhuǎn)調(diào)一層),有這個統(tǒng)一封裝類來遵循這個模塊的特有協(xié)議。筆者在這個時候已經(jīng)有了一個保底方案,就是把這個統(tǒng)一封裝對外所有暴露的方法和屬性寫進(jìn)這個Protocol中,然后把這個Protocol放入到Base層,模塊間通訊通過類似于[id<protocol>manager XXXXX:XXX];這樣的方式調(diào)用,這樣就把強依賴變成了弱依賴,是可以實現(xiàn)可選集成的,但是隨之而來的要求讓這個方案徹底被否決。

這里是一個插曲,幾個大部門老大聯(lián)合發(fā)話(主要是針對我們部門,我們部門的SDK最大,影響最深),不僅要實現(xiàn)功能可選集成,還要實現(xiàn)可擴(kuò)展,例如AI能力,我們現(xiàn)在有五六種,以后出現(xiàn)新的能力,能夠通過某種規(guī)則非常簡單增加一種AI能力,不能對現(xiàn)有代碼進(jìn)行改動。剛才哪種方案很明顯不行了,因為我們需要在上層重新寫調(diào)用Protocol的代碼。
第五步:不得不承認(rèn),我這里受到了阿里的BeeHive團(tuán)隊的啟發(fā)并進(jìn)行了深度改造,由于有新增能力不得對現(xiàn)有代碼進(jìn)行改動的要求,這就決定了,我的中介者不止有一個,是多個中介者,例如對AI能力有一個中介者,對所有AI能力負(fù)責(zé),能力庫有一個中介者,能力組裝庫也有一個中介者。這個中介者的有一個作用,上層對下層調(diào)用的統(tǒng)一入口,數(shù)據(jù)傳輸給中介者,由中介者對該層的所有組件進(jìn)行數(shù)據(jù)分發(fā),組件數(shù)據(jù)處理結(jié)束之后,再由中介者統(tǒng)一收集,由上層自行拿去,第二個不同的地方是,BeeHive方法的核心是每個組件有一個Protocol,作為標(biāo)識符性質(zhì)的協(xié)議,我這里有了一個BaseProtocol規(guī)范該層所有組件,SpecialProtocol用以作為標(biāo)識符性質(zhì)的協(xié)議,為什么用協(xié)議,不用字符串,是為了擴(kuò)展。在SDK啟動之初,所有的組件向該層中介者進(jìn)行注冊,告知中介者,我們有這個能力(這里我們經(jīng)過線程和算法的優(yōu)化,不必?fù)?dān)心性能問題)。于是調(diào)用邏輯變成了這樣,當(dāng)數(shù)據(jù)來臨的時候,最上層的中介者拿到數(shù)據(jù)分發(fā)給所有組件(如果沒有該組件,對nil對象發(fā)送消息,不會崩潰,這個大家都知道),所有組件根據(jù)自己需要的東西向下一層的中介者發(fā)送數(shù)據(jù)并等待結(jié)果,下一層的中介者再分發(fā)給本層的所有組件,到最底層處理結(jié)束后又由反方向返回數(shù)據(jù),最終將在最上層中介者告訴App,由于有BaseProtocol的存在,所以每一層的中介者對于本層的所有組件都是弱依賴,又由于SpecialProtocol的存在,能夠讓中介者準(zhǔn)確地分發(fā)給對應(yīng)組件,這個BaseProtocol能夠存在得益于我第二,三步橫縱向切割的正確性,如果切割不對,這個BaseProtocol將無法存在。

補充說明:
1:關(guān)于數(shù)據(jù)傳遞這一塊,我非常贊同Mediator作者關(guān)于數(shù)據(jù)傳遞需要去模型化傳遞的思想,如果一個自定義模型需要被傳遞(首先這里已經(jīng)不存在URL關(guān)于非常規(guī)參數(shù)無法傳遞的問題),需要去模型化,還有其他幾種情況,如果在開發(fā)過程中,如果一個數(shù)據(jù)被大量使用,是可以下沉到基礎(chǔ)層之類的地方去,如果僅有少部分組件實用,我目前的想法是我寧可復(fù)制一個類出來,如果讀者有更好的方案,歡迎討論。同時需要注意一點的是,如果某種特殊情況需要實現(xiàn)例如A->B->C這樣的傳遞,其中A和C需要使用到該數(shù)據(jù),B只需要傳遞就行了的情況,那么只有A和C需要知道具體類型,B是可以使用多態(tài)的,這在某些情況下非常實用。

2:關(guān)于同一層級組件如果需要進(jìn)行通訊,在我的案例里面,通過中介者,拿到另一個組件相關(guān)數(shù)據(jù)(去模型化的)

3:單一設(shè)計原理,不要出現(xiàn)類似于common這樣的東西,這會成為耦合的重災(zāi)區(qū)

4:淺繼承,繼承是面向?qū)ο笳Z言的優(yōu)勢,但是也是劣勢,會出現(xiàn)強耦合和超級類,cat大神在說Swift面向協(xié)議編程的時候說過這個問題

由于我們的SDK對公司內(nèi)都不開源,所以我沒有辦法把事情講得很清楚,更不可能貼代碼,如果我們有開源的那一天,希望能和大家細(xì)細(xì)討論,我最滿意的是能力組裝層的Center-Classify遞歸分發(fā)的設(shè)計。最后,沒有最好的設(shè)計,只有最合適的設(shè)計。勿噴,謝謝

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容