自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

美團(tuán)外賣(mài)Android平臺(tái)化架構(gòu)演進(jìn)實(shí)踐

移動(dòng)開(kāi)發(fā) Android
美團(tuán)外賣(mài)自 2013 年創(chuàng)建以來(lái),業(yè)務(wù)一直高速發(fā)展。目前美團(tuán)外賣(mài)日完成訂單量已突破 1800 萬(wàn),成為美團(tuán)點(diǎn)評(píng)最重要的業(yè)務(wù)之一。

美團(tuán)外賣(mài)自 2013 年創(chuàng)建以來(lái),業(yè)務(wù)一直高速發(fā)展。目前美團(tuán)外賣(mài)日完成訂單量已突破 1800 萬(wàn),成為美團(tuán)點(diǎn)評(píng)最重要的業(yè)務(wù)之一。

[[224117]]

美團(tuán)外賣(mài)的用戶(hù)端入口,從單一的外賣(mài)獨(dú)立 App,拓展為外賣(mài)、美團(tuán)、點(diǎn)評(píng)等多個(gè) App 入口。

美團(tuán)外賣(mài)所承載的業(yè)務(wù),也從單一的餐飲業(yè)務(wù),發(fā)展到餐飲、超市、生鮮、果蔬、藥品、鮮花、蛋糕、跑腿等十多個(gè)大品類(lèi)業(yè)務(wù)。業(yè)務(wù)的快速發(fā)展對(duì)客戶(hù)端架構(gòu)不斷提出新的挑戰(zhàn)。

平臺(tái)化背景

很早之前,外賣(mài)作為孵化中的項(xiàng)目只有美團(tuán)外賣(mài) App(下文簡(jiǎn)稱(chēng)外賣(mài) App)一個(gè)入口,后來(lái)外賣(mài)作為一個(gè)子頻道接入到美團(tuán) App(下文簡(jiǎn)稱(chēng)外賣(mài)頻道),兩端業(yè)務(wù)并行迭代開(kāi)發(fā)。

早期為了快速上線(xiàn),開(kāi)發(fā)同學(xué)直接將外賣(mài) App 的代碼拷貝一份到外賣(mài)頻道,做了簡(jiǎn)單的適配就很快接入到美團(tuán) App 了。

早期的外賣(mài) App 和外賣(mài)頻道由兩個(gè)團(tuán)隊(duì)分別維護(hù),而在隨后一段時(shí)間里,兩端代碼體系差異越來(lái)越大。

最后演變成了從網(wǎng)絡(luò)、圖片等基礎(chǔ)庫(kù)到 UI 控件、類(lèi)的命名等都不盡相同的兩套代碼。

盡管后來(lái)兩個(gè)團(tuán)隊(duì)合并到一起,但歷史的差異已經(jīng)形成,為了優(yōu)先滿(mǎn)足業(yè)務(wù)需求,很長(zhǎng)一段時(shí)間內(nèi),我們只能在兩套代碼的基礎(chǔ)上不斷堆積更多的功能。

維護(hù)兩套代碼的成本可想而知,而業(yè)務(wù)的迅猛發(fā)展又使得這一問(wèn)題越發(fā)不可忍受。

在我們探索解決兩端代碼復(fù)用的同時(shí),業(yè)務(wù)的發(fā)展又對(duì)我們提出新的挑戰(zhàn)。隨著團(tuán)隊(duì)成員擴(kuò)充了數(shù)倍,商超生鮮等垂直品類(lèi)的拆分,以及異地研發(fā)團(tuán)隊(duì)的建立,外賣(mài)客戶(hù)端的平臺(tái)化被提上日程。

而在此之前,外賣(mài) App 和外賣(mài)頻道基本保持單工程開(kāi)發(fā),這樣的模式顯然是無(wú)法支持多團(tuán)隊(duì)協(xié)作開(kāi)發(fā)的。

因此,我們需要快速將代碼重構(gòu)為支持平臺(tái)化的多工程模式,同時(shí)還要考慮業(yè)務(wù)模塊的解耦,使得新業(yè)務(wù)可以拷貝現(xiàn)有的代碼快速上線(xiàn)。

此外,在實(shí)施平臺(tái)化的過(guò)程中,兩端代碼復(fù)用的問(wèn)題還沒(méi)有解決,如果兩端的代碼沒(méi)有統(tǒng)一而直接做平臺(tái)化業(yè)務(wù)拆庫(kù),必然會(huì)導(dǎo)致問(wèn)題的復(fù)雜化。

在這樣的背景下,可以看出我們面臨的問(wèn)題相較于其他平臺(tái)型 App 更為特殊和復(fù)雜:既要解決外賣(mài)業(yè)務(wù)平臺(tái)化的問(wèn)題,又要解決外賣(mài) App 和外賣(mài)頻道兩端代碼復(fù)用的問(wèn)題。

屢次探索

在實(shí)施平臺(tái)化和兩端代碼復(fù)用的道路上并非一帆風(fēng)順,很多方案只有在嘗試之后才知道問(wèn)題所在。

我們多次遇到這樣的情況:設(shè)計(jì)方案完成后,團(tuán)隊(duì)已經(jīng)全身心投入到開(kāi)發(fā)之中,但是由于業(yè)務(wù)形態(tài)發(fā)生變化,原有的設(shè)計(jì)也被迫更改。

在不斷的探索和實(shí)踐過(guò)程中,我們經(jīng)歷了多個(gè)中間階段。雖然有不少失敗的案例,但是也積累了很多架構(gòu)設(shè)計(jì)上的寶貴經(jīng)驗(yàn),整個(gè)團(tuán)隊(duì)對(duì)業(yè)務(wù)和架構(gòu)也有了更深的理解。

搜索庫(kù)拆分實(shí)踐

早期美團(tuán)外賣(mài) App 和美團(tuán)外賣(mài)頻道兩個(gè)團(tuán)隊(duì)的合并,帶來(lái)的最大痛點(diǎn)是代碼復(fù)用,而非平臺(tái)化,而在很長(zhǎng)的一段時(shí)間內(nèi),我們也沒(méi)有想過(guò)從平臺(tái)化的角度去解決兩端代碼復(fù)用的問(wèn)題。

然而代碼復(fù)用的一些失敗嘗試,給后續(xù)平臺(tái)化的架構(gòu)帶來(lái)了不少寶貴的經(jīng)驗(yàn)。

當(dāng)時(shí)是怎么解決代碼復(fù)用問(wèn)題的呢?我們通過(guò)和產(chǎn)品、設(shè)計(jì)同學(xué)的溝通,約定了未來(lái)的需求,會(huì)從需求內(nèi)容、交互、樣式上,兩端盡可能的保持一致。

經(jīng)過(guò)多次討論后,團(tuán)隊(duì)發(fā)起了兩端代碼復(fù)用的技術(shù)方案嘗試,我們決定將搜索模塊從主工程拆分出來(lái),并實(shí)現(xiàn)兩端代碼復(fù)用。

然而兩端的搜索模塊代碼底層差異很大,BaseActivity 和 BaseFragment 不統(tǒng)一,UI 樣式不統(tǒng)一,數(shù)據(jù) Model 不統(tǒng)一,圖片、網(wǎng)絡(luò)、埋點(diǎn)不統(tǒng)一,并且兩端發(fā)版周期也不一致。

針對(duì)這些問(wèn)題的解決方案是:

  • 通過(guò)代理屏蔽 Activity 和 Fragment 基類(lèi)不統(tǒng)一的問(wèn)題。
  • 兩端主工程 style 覆蓋搜索庫(kù)的 UI 樣式。
  • 搜索庫(kù)使用獨(dú)立的數(shù)據(jù) Model,上層去做數(shù)據(jù)適配。
  • 其他差異通通拋出接口讓上層實(shí)現(xiàn)。
  • 和 PM 溝通盡量使產(chǎn)品需求和發(fā)版周期一致。

大致架構(gòu)如下圖:

雖然搜索庫(kù)在短期內(nèi)拆分為獨(dú)立的工程,并實(shí)現(xiàn)了絕大部分的兩端代碼復(fù)用。

但是好景不長(zhǎng),僅僅更新過(guò)幾個(gè)版本后,由于需求和版本發(fā)布周期的差異,搜索庫(kù)開(kāi)始變?yōu)閮蓚€(gè)分支,并且兩個(gè)分支的差異越來(lái)越大,最后代碼無(wú)法合并而不得不永久維護(hù)兩個(gè)搜索庫(kù)。

搜索庫(kù)事實(shí)上是一次失敗的拆分,其中的問(wèn)題總結(jié)起來(lái)有三個(gè):

  • 在兩端底層差異巨大的情況下自上而下的強(qiáng)行拆分,導(dǎo)致大量實(shí)現(xiàn)和適配留在了兩端主工程實(shí)現(xiàn),這樣的設(shè)計(jì)層級(jí)混亂,邊界模糊,并且極大的增加了業(yè)務(wù)開(kāi)發(fā)的復(fù)雜性。
  • 寄希望于兩端需求和發(fā)版周期完全一致這個(gè)想法不切實(shí)際,如果在架構(gòu)上不為兩端的差異性預(yù)留可伸縮的空間,復(fù)用最終是難以持續(xù)的。
  • 約定或規(guī)范,受限于組織架構(gòu)和具體執(zhí)行的個(gè)人,不確定性太高。

頁(yè)面組件化實(shí)踐

在經(jīng)歷過(guò)搜索庫(kù)的失敗拆分后,大家認(rèn)為目前還不具備實(shí)現(xiàn)模塊整體拆分和復(fù)用的條件,因此我們走向了另一個(gè)方向,即實(shí)現(xiàn)頁(yè)面的組件化以達(dá)成部分組件復(fù)用的目標(biāo)。

頁(yè)面組件化的設(shè)計(jì)思路是:

  • 將頁(yè)面拆分為粒度更小的組件,組件內(nèi)部除了包含 UI 實(shí)現(xiàn),還包含數(shù)據(jù)層和邏輯層。
  • 組件提供個(gè)性化配置滿(mǎn)足兩端差異需求,如果無(wú)法滿(mǎn)足再通過(guò)代理拋到上層處理。

頁(yè)面組件化是一個(gè)良好的設(shè)計(jì),但它主要適用于解決 Activity 巨大化的問(wèn)題。

由于底層差異巨大的情況,使得頁(yè)面組件化很難實(shí)現(xiàn)大規(guī)模的復(fù)用,復(fù)用效率低。另一方面,頁(yè)面組件化也沒(méi)有為兩端差異性預(yù)留可伸縮的空間。

MVP 分層復(fù)用實(shí)踐

我們還嘗試過(guò)運(yùn)用設(shè)計(jì)模式解決兩端代碼復(fù)用的問(wèn)題。想法是將代碼分為易變的和穩(wěn)定的兩部分,易變部分在兩端上層實(shí)現(xiàn)差異化處理,穩(wěn)定部分可以在下層實(shí)現(xiàn)復(fù)用。

方案的主要設(shè)計(jì)思路是:

  • 借鑒 Clean MVP 架構(gòu),根據(jù)職責(zé)將代碼拆分為 Presenter,Data Repository,Use Case,View,Model 等角色。
  • UI、動(dòng)畫(huà)、數(shù)據(jù)請(qǐng)求等邏輯在下層僅保留接口,在上層實(shí)現(xiàn)并注入到下層。
  • 對(duì)于兩端不一致的數(shù)據(jù) Model,通過(guò)轉(zhuǎn)換器適配為下層統(tǒng)一的模型。

大致架構(gòu)如下圖:

這是一種靈活、優(yōu)雅的設(shè)計(jì),能夠?qū)崿F(xiàn)部分代碼的復(fù)用,并能解決兩端基礎(chǔ)庫(kù)和 UI 等差異。

這個(gè)方案在首頁(yè)和二級(jí)頻道頁(yè)的部分模塊使用了一段時(shí)間,但是因?yàn)閷W(xué)習(xí)成本較高等原因推廣比較緩慢。

另外,這個(gè)時(shí)期平臺(tái)化已被提上日程,業(yè)務(wù)痛點(diǎn)決定了我們必須快速實(shí)施模塊整體的拆分和復(fù)用,而優(yōu)雅的設(shè)計(jì)模式并不適合解決這一類(lèi)問(wèn)題。

即使從復(fù)用性的角度來(lái)看,這樣的設(shè)計(jì)也會(huì)使得業(yè)務(wù)開(kāi)發(fā)變得更為復(fù)雜、調(diào)試?yán)щy,對(duì)于新人來(lái)說(shuō)難以勝任,最終推廣落地困難。

中間層實(shí)踐

通過(guò)多次實(shí)踐,我們認(rèn)識(shí)到要實(shí)現(xiàn)兩端代碼復(fù)用,基礎(chǔ)庫(kù)的統(tǒng)一是必然的工作,是其他一切工作的基礎(chǔ)。否則必然導(dǎo)致復(fù)雜和難以維護(hù)的設(shè)計(jì),最終導(dǎo)致兩端復(fù)用無(wú)法快速推進(jìn)下去。

計(jì)算機(jī)界有一句名言:“計(jì)算機(jī)科學(xué)領(lǐng)域的任何問(wèn)題都可以通過(guò)增加一個(gè)中間層來(lái)解決。”(原始版本出自計(jì)算機(jī)科學(xué)家 David Wheeler)我們當(dāng)然有想過(guò)通過(guò)中間層設(shè)計(jì)屏蔽兩端的基礎(chǔ)庫(kù)差異。

例如網(wǎng)絡(luò)庫(kù),外賣(mài) App 基于 Volley 實(shí)現(xiàn),外賣(mài)頻道基于 Retrofit 實(shí)現(xiàn)。我們?cè)?jīng)在 Volley 和 Retrofit 之上封裝了一層網(wǎng)絡(luò)框架,對(duì)外暴露統(tǒng)一的接口,上層可以切換底層依賴(lài) Volley 或是 Retrofit。

但這個(gè)中間層并沒(méi)有上線(xiàn),最終我們將兩端的網(wǎng)絡(luò)庫(kù)統(tǒng)一成了 Retrofit。

這里面有多個(gè)原因:

  • 首先 Retrofit 本身就是較高層次的封裝,并且擁有優(yōu)雅的設(shè)計(jì)模式,理論上我們很難封裝一套擴(kuò)展性更強(qiáng)的接口。
  • 其次長(zhǎng)期來(lái)看底層網(wǎng)絡(luò)框架變更的風(fēng)險(xiǎn)極低,并且適配網(wǎng)絡(luò)層的各種插件也是一件費(fèi)時(shí)費(fèi)力的事情,因此保持網(wǎng)絡(luò)中間層的性?xún)r(jià)比極低。
  • 此外將兩端的網(wǎng)絡(luò)請(qǐng)求都替換為中間層接口,顯然工作量遠(yuǎn)大于只保留一端的依賴(lài)。

通過(guò)實(shí)踐我們認(rèn)識(shí)到,中間層設(shè)計(jì)是一把雙刃劍。如果基礎(chǔ)框架本身的擴(kuò)展性足夠強(qiáng),中間層設(shè)計(jì)就顯得多此一舉,甚至喪失了原有框架的良好特性。

平臺(tái)化實(shí)踐

好的架構(gòu)源于不停地衍變,而非設(shè)計(jì)。對(duì)于外賣(mài) Android 客戶(hù)端的平臺(tái)化架構(gòu)構(gòu)建也是經(jīng)歷了同樣的過(guò)程。

我們從考慮如何解決代碼復(fù)用的問(wèn)題,逐漸的衍變成如何去解決代碼復(fù)用和平臺(tái)化的兩個(gè)問(wèn)題。而實(shí)際上外賣(mài)平臺(tái)化正是解決兩端代碼復(fù)用的一劑良藥。

我們通過(guò)建立外賣(mài)平臺(tái),將現(xiàn)有的外賣(mài)業(yè)務(wù)降級(jí)為一個(gè)頻道,將外賣(mài)業(yè)務(wù)以 aar 的形式分別接入到外賣(mài)平臺(tái)和美團(tuán)平臺(tái)。這樣在解決外賣(mài)平臺(tái)化的同時(shí),代碼復(fù)用的問(wèn)題也將得到完美的解決。

平臺(tái)化架構(gòu)

經(jīng)過(guò)了整整一年的艱苦奮斗,形成了如圖所示的美團(tuán)外賣(mài) Android 客戶(hù)端平臺(tái)化架構(gòu),如下圖:

從底層到高層依次為平臺(tái)層、業(yè)務(wù)層和宿主層:

  • 平臺(tái)層,內(nèi)容包括承載上層的數(shù)據(jù)通信和頁(yè)面跳轉(zhuǎn);提供外賣(mài)核心服務(wù),例如商品管理、訂單管理、購(gòu)物車(chē)管理等;提供配置管理服務(wù);提供統(tǒng)一的基礎(chǔ)設(shè)施能力,例如網(wǎng)絡(luò)、圖片、監(jiān)控、報(bào)警、定位、分享、熱修、埋點(diǎn)、Crash 上報(bào)等;提供其他管理能力,例如生命周期管理、組件化等。
  • 業(yè)務(wù)層,內(nèi)容包括外賣(mài)業(yè)務(wù)和垂直業(yè)務(wù)。
  • 宿主層,內(nèi)容包括 Waimai App 殼和美團(tuán)外賣(mài)頻道 Waimai-channel 殼,這一層用于 Application 的初始化、dex 加載和其他各種必要的組件或基礎(chǔ)庫(kù)的初始化。

在構(gòu)建平臺(tái)化架構(gòu)的過(guò)程中,我們遇到這樣一個(gè)問(wèn)題,如何長(zhǎng)久的維持我們平臺(tái)化架構(gòu)的層級(jí)邊界。

試想,如果所有的代碼都在一個(gè)工程里面開(kāi)發(fā),通過(guò)包名、約定去規(guī)范層級(jí)邊界,任何一個(gè)緊急的需求都可能破壞層級(jí)邊界。

維持層級(jí)邊界的最好辦法是什么?我們的經(jīng)驗(yàn)是工程隔離。

平臺(tái)化的每一層都去做工程隔離,業(yè)務(wù)層的每個(gè)業(yè)務(wù)都建立自己的工程庫(kù),實(shí)現(xiàn)工程隔離。同時(shí),配套編譯腳本,檢查業(yè)務(wù)庫(kù)之間是否存在相互依賴(lài)關(guān)系。

工程隔離的好處是顯而易見(jiàn)的:

每個(gè)工程都可以獨(dú)立編譯、獨(dú)立打包。

每個(gè)工程內(nèi)部的修改,不會(huì)影響其他工程。

業(yè)務(wù)庫(kù)工程可以快速拆分出來(lái),集成到其他 App 中。

但工程隔離帶來(lái)的另一個(gè)問(wèn)題是,同層間的業(yè)務(wù)庫(kù)需要通信怎么辦?這時(shí)候就需要提供業(yè)務(wù)庫(kù)通信框架來(lái)解決這個(gè)問(wèn)題。

業(yè)務(wù)庫(kù)通信框架

在拆分外賣(mài)商家業(yè)務(wù)庫(kù)的時(shí)候,我們就發(fā)這樣一個(gè)案例:在商家頁(yè)有一個(gè)業(yè)務(wù),當(dāng)發(fā)現(xiàn)當(dāng)前商家是打烊的,就會(huì)彈出一個(gè)浮層,推薦相似的商家列表,而在我們之前劃分的外賣(mài)子業(yè)務(wù)庫(kù)里面,相似商家列表應(yīng)該是屬于頁(yè)面庫(kù)里面的內(nèi)容。

那怎么讓商家業(yè)務(wù)庫(kù)訪(fǎng)問(wèn)到頁(yè)面庫(kù)里面的代碼呢?如果我們將商家?guī)烊ヒ蕾?lài)頁(yè)面庫(kù),那我們的層級(jí)邊界就會(huì)被打破,我們的依賴(lài)關(guān)系也會(huì)變得復(fù)雜。

因此我們需要在架構(gòu)中提供同層間的通信框架,它去解決不打破層級(jí)邊界的情況下,完成同層間的通信。

匯總同層間通信的場(chǎng)景,大致上可以劃分為:

  • 頁(yè)面的跳轉(zhuǎn)。
  • 基本數(shù)據(jù)類(lèi)型的傳遞(包括可序列化的共有類(lèi)對(duì)象的傳遞)。
  • 模塊內(nèi)部自定義方法和類(lèi)的調(diào)用。

針對(duì)上述情況,在我們的架構(gòu)里面提供了二種平級(jí)間的通信方式:scheme 路由和美團(tuán)自建的 ServiceLoaders sdk。

scheme 路由本質(zhì)上是利用 Android 的 scheme 原理進(jìn)行通信,ServiceLoader 本質(zhì)上是利用的 Java 反射機(jī)制進(jìn)行通信。

scheme 路由的調(diào)用如圖所示:

最終效果:所有業(yè)務(wù)頁(yè)面的跳轉(zhuǎn),都需要通過(guò)平臺(tái)層的 scheme 路由去分發(fā)。通過(guò) scheme 路由,所有業(yè)務(wù)都得到解耦,不再需要相互依賴(lài)而可以實(shí)現(xiàn)頁(yè)面的跳轉(zhuǎn)和基本數(shù)據(jù)類(lèi)型的傳遞。

serviceloader 的調(diào)用如圖所示:

提供方和使用方通過(guò)平臺(tái)層的一個(gè)接口作為雙方交互的約束。使用方通過(guò)平臺(tái)層的 ServiceLoader 完成提供方的實(shí)現(xiàn)對(duì)象獲取。

這種方式可以解決模塊內(nèi)部自定義方法和類(lèi)的調(diào)用,例如我們之前提到了商家?guī)煨枰{(diào)用頁(yè)面庫(kù)代碼的問(wèn)題就可以通過(guò) ServiceLoader 解決。

外賣(mài)內(nèi)核模塊設(shè)計(jì)

在實(shí)踐的過(guò)程中,我們也遇到業(yè)務(wù)本身就不好劃分層級(jí)邊界的業(yè)務(wù)。大家可以從美團(tuán)外賣(mài)三層架構(gòu)圖上,看出外賣(mài)業(yè)務(wù)庫(kù),像商家、訂單等,是和外賣(mài)的垂類(lèi)業(yè)務(wù)庫(kù)是同級(jí)的。

而實(shí)際上外賣(mài)業(yè)務(wù)的子業(yè)務(wù)是否應(yīng)該和垂類(lèi)業(yè)務(wù)保持同層是一個(gè)目前無(wú)法確定的事情。

目前,外賣(mài)接入的垂類(lèi)業(yè)務(wù)商超業(yè)務(wù),是隸屬于外賣(mài)業(yè)務(wù)的子頻道,它依然依賴(lài)著外賣(mài)的核心 Model、核心服務(wù),包括商品管理、訂單管理、購(gòu)物車(chē)管理等。

因此目前它和外賣(mài)業(yè)務(wù)的商家、訂單這樣的子業(yè)務(wù)庫(kù)同層是沒(méi)有問(wèn)題的。但隨著商超業(yè)務(wù)的發(fā)展,商超業(yè)務(wù)未來(lái)可能會(huì)建設(shè)自己的商品管理、訂單管理、購(gòu)物車(chē)管理的服務(wù),那么到時(shí)商超業(yè)務(wù)就會(huì)上升到和外賣(mài)業(yè)務(wù)一樣同層的業(yè)務(wù)。

這時(shí)候,外賣(mài)核心管理服務(wù),處在平臺(tái)層,就會(huì)導(dǎo)致架構(gòu)的層級(jí)邊界變得不再清晰。

我們的解決辦法是通過(guò)設(shè)計(jì)一個(gè)屬于外賣(mài)業(yè)務(wù)的內(nèi)核模塊來(lái)適應(yīng)未來(lái)的變化,內(nèi)核模塊的設(shè)計(jì)如圖:

  • 內(nèi)圈為基礎(chǔ)模型類(lèi),這些模型類(lèi)構(gòu)成了外賣(mài)核心業(yè)務(wù)(從門(mén)店→點(diǎn)菜→購(gòu)物車(chē)→訂單)的基礎(chǔ)。
  • 中間圈為依賴(lài)基礎(chǔ)模型類(lèi)構(gòu)建的基礎(chǔ)服務(wù)(CRUD)。
  • 最外圈為外賣(mài)的各維度業(yè)務(wù),向內(nèi)依賴(lài)基礎(chǔ)模型圈和外賣(mài)基礎(chǔ)服務(wù)圈。

如果未來(lái)確定外賣(mài)平臺(tái)需要接入更多和外賣(mài)平級(jí)的業(yè)務(wù),且最內(nèi)圈都完全不一樣,我們將把外賣(mài)內(nèi)核模塊上移,在外賣(mài)業(yè)務(wù)子庫(kù)下建立對(duì)內(nèi)核模塊的依賴(lài)。

如果未來(lái)只是有更多的外賣(mài)子業(yè)務(wù)的接入,那就繼續(xù)保留我們現(xiàn)在的架構(gòu);如果未來(lái)接入的業(yè)務(wù)基礎(chǔ)模型類(lèi)一樣,但自己的業(yè)務(wù)服務(wù)需要分化,那么我們將保留內(nèi)核模塊最核心的內(nèi)圈,并抽象出服務(wù)層由外賣(mài)和商超上層自己實(shí)現(xiàn)真正的服務(wù)。

業(yè)務(wù)庫(kù)拆分

在拆分業(yè)務(wù)庫(kù)的時(shí)候,我們面臨著這樣的問(wèn)題:業(yè)務(wù)之間的關(guān)系是較為復(fù)雜的,如何去拆分業(yè)務(wù)庫(kù),才是較為合理的呢?

一開(kāi)始我們準(zhǔn)備根據(jù)外賣(mài)業(yè)務(wù)核心流程:頁(yè)面→商家→下單,去拆分外賣(mài)業(yè)務(wù)。

但是隨著外賣(mài)子頻道業(yè)務(wù)的快速發(fā)展,子頻道業(yè)務(wù)也建立了自己的研發(fā)團(tuán)隊(duì),在頁(yè)面、商家、下單等環(huán)節(jié),也開(kāi)始建立自己的頁(yè)面。

如果我們?nèi)匀话凑胀赓u(mài)下單的流程去拆分庫(kù),那在同一個(gè)庫(kù)之間,就會(huì)有外賣(mài)團(tuán)隊(duì)和外賣(mài)子頻道團(tuán)隊(duì)共同開(kāi)發(fā)的情況,這樣職責(zé)邊界很不清晰,在實(shí)際的開(kāi)發(fā)過(guò)程中,肯定會(huì)出現(xiàn)理不清的情況。

我們都知道軟件工程領(lǐng)域有所謂的康威定律:

Organizations which design systems are constrained to produce designs which are copies of the communication structures of these organizations. 

- Melvin Conway(1967)

翻譯成中文的大概意思是:設(shè)計(jì)系統(tǒng)的組織,其產(chǎn)生的設(shè)計(jì)等同于組織之內(nèi)、組織之間的溝通結(jié)構(gòu)。

在康威定理的指導(dǎo)下:我們認(rèn)為技術(shù)架構(gòu)應(yīng)該反映出團(tuán)隊(duì)的組織結(jié)構(gòu),同時(shí),組織結(jié)構(gòu)的變遷,也應(yīng)該導(dǎo)致技術(shù)架構(gòu)的演進(jìn)。

美團(tuán)外賣(mài)平臺(tái)下包含外賣(mài)業(yè)務(wù)和垂直品類(lèi)業(yè)務(wù),對(duì)于在我們團(tuán)隊(duì)中已經(jīng)有了組織結(jié)構(gòu),優(yōu)先組織結(jié)構(gòu),去拆出獨(dú)立的業(yè)務(wù)庫(kù),方便子業(yè)務(wù)庫(kù)的同學(xué)內(nèi)部溝通協(xié)作,減少他們跨組織溝通的成本。

同時(shí),我們將負(fù)責(zé)外賣(mài)業(yè)務(wù)的大團(tuán)隊(duì),再進(jìn)一步細(xì)化成頁(yè)面小組、商家小組和訂單小組,由這些小組的同學(xué)去在外賣(mài)業(yè)務(wù)下完成更細(xì)維度的外賣(mài)子業(yè)務(wù)庫(kù)拆分。

根據(jù)組織結(jié)構(gòu)劃分的業(yè)務(wù)庫(kù),天然的存在業(yè)務(wù)邊界,每個(gè)同學(xué)都會(huì)按照自己業(yè)務(wù)的目標(biāo)去繼續(xù)完善自己的業(yè)務(wù)庫(kù)。這樣的拆庫(kù)對(duì)內(nèi)是高內(nèi)聚,對(duì)外是低耦合的,有效的降低了內(nèi)外溝通協(xié)作的成本。

工程內(nèi)代碼隔離

在實(shí)現(xiàn)工程隔離之后,我們發(fā)現(xiàn)工程內(nèi)部的代碼還是可以相互引用的。工程內(nèi)部如果也不能實(shí)現(xiàn)代碼的隔離,那么工程內(nèi)部的邊界就是模糊的。

我們希望工程內(nèi)至少能夠?qū)崿F(xiàn)頁(yè)面級(jí)別的代碼隔離,因?yàn)?Activity 是組成一個(gè) App 的頁(yè)面單元,圍繞這個(gè) Activity,通常會(huì)有大量的代碼及資源文件,我們希望這些代碼和資源文件是被集中管理的。

通常我們想到的做法是以 module 工程為單位的相互隔離,但在 module 是相對(duì)比較重的一個(gè)約束,難道每個(gè) Activity 都要建一個(gè) module 嗎?

這樣代碼結(jié)構(gòu)會(huì)變得很復(fù)雜,而且針對(duì)一些大的業(yè)務(wù)體,又會(huì)形成巨大化的 module。

那我們又想到規(guī)范代碼,用包名去人為約定,但靠包名約束的代碼,邊界模糊,時(shí)不時(shí)的緊急需求,就把包名約定打破了,而且資源文件的擺放也是任意的,遷移成本高。

那怎么去解決工程內(nèi)部的邊界問(wèn)題呢?《微信的模塊化架構(gòu)重構(gòu)實(shí)踐》一文中提到了一個(gè)重要的概念 p(pins)工程,p 工程可謂是工程內(nèi)約束代碼邊界的重要法寶。

通過(guò)在 Gradle 里面配置 sourceSets,就可以改變工程內(nèi)的代碼結(jié)構(gòu)目錄,完成代碼的隔離,配置示例:

效果如圖所示:

從上圖可以看出,這個(gè)業(yè)務(wù)庫(kù)被以頁(yè)面為單元拆分成了多個(gè) p 工程,每個(gè) p 工程的邊界都是清楚的,實(shí)現(xiàn)了工程內(nèi)的代碼隔離。

工程內(nèi)代碼隔離帶來(lái)的好處顯而易見(jiàn):

  • p 工程實(shí)現(xiàn)了最小粒度的代碼邊界約束。
  • 工程內(nèi)模塊職責(zé)清晰。
  • 業(yè)務(wù)模塊可以被快速的拆分出來(lái)。

代碼復(fù)用

p 工程滿(mǎn)足了工程內(nèi)代碼隔離的需求,但是別忘了,我們每個(gè)模塊在外賣(mài)兩個(gè)終端上(外賣(mài) App&美團(tuán) App)上可能存在差異,如果能在模塊內(nèi)部實(shí)現(xiàn)兩端差異,我們的目標(biāo)才算達(dá)成。

基于上述考慮,我們想到了使用 Gradle 提供的 productFlavors 來(lái)實(shí)現(xiàn)兩端的差異化。

為此,我們需要定義兩個(gè) flavor:wm 和 mt。

但是,這樣生成的 p 工程是并列的,也就是說(shuō),各個(gè) p 工程中所有的差異化代碼都需要被存放在這兩個(gè) flavor 對(duì)應(yīng)的 SourceSet 下,這豈不是跟模塊間代碼隔離的理念相違背?

理想的結(jié)構(gòu)是在 p 工程內(nèi)部進(jìn)行 flavor 劃分,由 p 工程內(nèi)部包容差異化,繼續(xù)改成 Gradle 腳本如下:

最終工程結(jié)構(gòu)變成如下:

通過(guò) p 工程和 flavor 的靈活應(yīng)用,我們最終將業(yè)務(wù)庫(kù)配置成以 p 工程為維度的模塊單元,并在 p 工程內(nèi)部兼容兩端的共性及差異,代碼復(fù)用被很好的解決了。

同時(shí),兩端差異的問(wèn)題是歸屬在 p 工程內(nèi)部自己處理的,并沒(méi)有建立中間層,或?qū)⒉町悞伣o上層殼工程去完成,這樣的設(shè)計(jì)遵守了邊界清晰,向下依賴(lài)的原則。

但是,工程內(nèi)隔離也存在與工程隔離一樣的問(wèn)題:同層級(jí) p 工程需要通信怎么辦?

我們?cè)诓鸱稚碳規(guī)斓臅r(shí)候,就面臨這這樣的問(wèn)題,商品活動(dòng)頁(yè)和商品詳情頁(yè),可以根據(jù)頁(yè)面維度,去拆分成 2 個(gè) p 工程,這兩個(gè)頁(yè)面都會(huì)用到同一個(gè)商品樣式的 item。

如何讓同層間商品活動(dòng)頁(yè) p 工程和商品詳情頁(yè) p 工程訪(fǎng)問(wèn)到商品樣式 item呢?

在實(shí)際拆庫(kù)的實(shí)踐中,我們逐漸的探索出三級(jí)工程結(jié)構(gòu)。三級(jí)工程結(jié)構(gòu)不僅可以解決工程內(nèi)p工程通信的問(wèn)題,而且可以保持架構(gòu)的靈活性。

三級(jí)工程結(jié)構(gòu)

三級(jí)工程結(jié)構(gòu),指的是工程→module→p 工程的三級(jí)結(jié)構(gòu)。

我們可以將任何一個(gè)非常復(fù)雜的業(yè)務(wù)工程內(nèi)部劃分成若干個(gè)獨(dú)立單元的 module 工程,同時(shí)獨(dú)立單元的 module 工程,我們可以繼續(xù)去劃分它內(nèi)部的獨(dú)立 p 工程。

因?yàn)?module 是具備編譯時(shí)的代碼隔離的,邊界是不容易被打破的,它可以隨時(shí)升級(jí)為一個(gè)工程。

需要通信的 p 工程依賴(lài) module 的主目錄,base 目錄,通過(guò)base目錄實(shí)現(xiàn)通信。

工程和 module 具有編譯上隔離代碼的能力,p 工程具有最小約束代碼邊界的能力,這樣的設(shè)計(jì)可以使得工程內(nèi)邊界清晰,向下依賴(lài)。

設(shè)計(jì)如圖所示:

三級(jí)工程結(jié)構(gòu)的最大好處就是,每級(jí)都可按照需要靈活的升級(jí)或降級(jí),這樣靈活的升降級(jí),可以隨時(shí)適應(yīng)團(tuán)隊(duì)組織結(jié)構(gòu)的變化,保持架構(gòu)拆分合并的靈活性,從而動(dòng)態(tài)的滿(mǎn)足了康威定理。

工程化建設(shè)

平臺(tái)化一個(gè)直觀的結(jié)果就是產(chǎn)生了很多子庫(kù),如何對(duì)這些子庫(kù)進(jìn)行有效的工程化管理將是一個(gè)影響團(tuán)隊(duì)研發(fā)效率的問(wèn)題。目前為止,我們從以下兩個(gè)方面做了改進(jìn)。

一鍵切源碼

主工程集成業(yè)務(wù)庫(kù)時(shí),有兩種依賴(lài)模式:aar 依賴(lài)和源碼依賴(lài)。默認(rèn)是 aar 依賴(lài),但是在平時(shí)開(kāi)發(fā)時(shí),經(jīng)常需要從 aar 依賴(lài)切換到源碼依賴(lài),比如新需求開(kāi)發(fā)、bugfix 及排查問(wèn)題等。

正常情況我們需要在各個(gè)工程的 build. 中將 compile aar 手動(dòng)改為 compile project,如果業(yè)務(wù)庫(kù)也需要依賴(lài)平臺(tái)庫(kù)源碼,也要做類(lèi)似的操作。

如下圖所示:

這樣手動(dòng)操作會(huì)帶來(lái)兩個(gè)問(wèn)題:

  • build.gradle 改動(dòng)頻繁,如果開(kāi)發(fā)人員不小心 push 上去了,將會(huì)造成各種沖突。
  • 當(dāng)業(yè)務(wù)庫(kù)越來(lái)越多時(shí),這種改動(dòng)的成本就越來(lái)越大了。

鑒于這種需求具備通用性,我們開(kāi)發(fā)了一個(gè) Gradle 插件,通過(guò)主工程的一個(gè)配置文件(被 git ignore),可一鍵切換至源碼依賴(lài)。

例如需要源碼依賴(lài)商家?guī)?,那么只需要在主工程中將該?kù)的源碼依賴(lài)開(kāi)關(guān)打開(kāi)即可。商家?guī)爝€依賴(lài)平臺(tái)庫(kù),默認(rèn)也是 aar 依賴(lài),如果想改成源碼依賴(lài),也只需把開(kāi)關(guān)打開(kāi)即可。

一鍵打包

業(yè)務(wù)庫(kù)增多以后,構(gòu)建流程也變得復(fù)雜起來(lái),我們交付的產(chǎn)物有兩種:外賣(mài) App 的 apk 和外賣(mài)頻道的 aar。

外賣(mài) App 的情況會(huì)簡(jiǎn)單一些,在 Jenkins 上關(guān)聯(lián)各個(gè)業(yè)務(wù)庫(kù)指定分支的源碼,直接打包即可。

而外賣(mài)頻道的情況則比較復(fù)雜,因?yàn)槭艿矫缊F(tuán)平臺(tái)的一些限制,頻道打包不能直接關(guān)聯(lián)各個(gè)業(yè)務(wù)庫(kù)的源碼,只能依賴(lài) aar。

按照傳統(tǒng)做法,需要逐個(gè)打業(yè)務(wù)庫(kù)的 aar,然后統(tǒng)一在頻道工程中集成,最后再打頻道 aar,這樣效率實(shí)在太低。為此,我們改進(jìn)了頻道的打包流程。

如下圖所示:

先打平臺(tái)庫(kù) aar,打完后自動(dòng)提 PR 到各個(gè)業(yè)務(wù)庫(kù)去修改平臺(tái)庫(kù)的版本號(hào),接著再逐個(gè)觸發(fā)業(yè)務(wù)庫(kù)去打 aar。

業(yè)務(wù)庫(kù)打完 aar 之后再自動(dòng)提 PR 到頻道主庫(kù)去修改業(yè)務(wù)庫(kù)的版本號(hào),等全部業(yè)務(wù)庫(kù) aar 打完后最后再自動(dòng)觸發(fā)打頻道主庫(kù)的 aar,至此一鍵打包完畢。

平臺(tái)化總結(jié)

從搜索庫(kù)拆分的第一次嘗試算起,外賣(mài) Android 客戶(hù)端在架構(gòu)上的持續(xù)探索和實(shí)踐已經(jīng)經(jīng)歷了 2 年多的時(shí)間。

起初為了解決兩端代碼復(fù)用的問(wèn)題,我們嘗試過(guò)自上而下的強(qiáng)行拆分和復(fù)用,但很快就暴露出層次混亂、邊界模糊帶來(lái)的問(wèn)題,并且認(rèn)識(shí)到如果不能提供兩端差異化的解決方案,代碼復(fù)用是很難持續(xù)的。

后來(lái)我們又嘗試過(guò)運(yùn)用設(shè)計(jì)模式約束邊界,先實(shí)現(xiàn)解耦再進(jìn)行復(fù)用,但在推廣落地過(guò)程中認(rèn)識(shí)到復(fù)雜的設(shè)計(jì)很難快速推進(jìn)下去。

在平臺(tái)化開(kāi)始的時(shí)候,團(tuán)隊(duì)已經(jīng)形成了設(shè)計(jì)簡(jiǎn)單、邊界清晰的架構(gòu)理念。我們將整體結(jié)構(gòu)劃分為宿主層、業(yè)務(wù)層、平臺(tái)層,并嚴(yán)格約束層次間的依賴(lài)關(guān)系。

在業(yè)務(wù)模塊拆分的過(guò)程中,我們借鑒微信的工程結(jié)構(gòu)方案,按照三級(jí)工程結(jié)構(gòu)劃分業(yè)務(wù)邊界,實(shí)現(xiàn)靈活的代碼隔離,并降低了后續(xù)模塊遷出和遷入成本,使得架構(gòu)動(dòng)態(tài)滿(mǎn)足康威定律。

在兩端代碼復(fù)用的問(wèn)題上,我們認(rèn)識(shí)到要實(shí)現(xiàn)可持續(xù)的代碼復(fù)用,必須自下向上的逐步統(tǒng)一兩端底層的基礎(chǔ)依賴(lài),同時(shí)又能容易的支持兩端上層業(yè)務(wù)的差異化處理。

使用 Flavor 管理兩端的差異代碼,盡量減少向上依賴(lài),在具體實(shí)施時(shí)應(yīng)用之前積累的解耦設(shè)計(jì)的經(jīng)驗(yàn),從而滿(mǎn)足了架構(gòu)的可伸縮性。

沒(méi)有一個(gè)方案能獲得每個(gè)人的贊同。在平臺(tái)化的實(shí)施過(guò)程中,團(tuán)隊(duì)成員多次對(duì)方案選型發(fā)生過(guò)針?shù)h相對(duì)的討論。

這時(shí)我們會(huì)拋開(kāi)技術(shù)方案,回到問(wèn)題本身,去重新審視業(yè)務(wù)的痛點(diǎn),列出要解決的問(wèn)題,再回過(guò)頭來(lái)看哪一個(gè)方案能夠解決問(wèn)題。

雖然我們并不常常這么做,但某些時(shí)刻也會(huì)強(qiáng)制決策和實(shí)施,遇到問(wèn)題再?gòu)?fù)盤(pán)和調(diào)整。

任何一種設(shè)計(jì)理念都有其適用場(chǎng)景。我們?cè)诓粩嚓P(guān)注業(yè)內(nèi)一些優(yōu)秀的架構(gòu)和設(shè)計(jì)理念,以及公司內(nèi)部美團(tuán) App、點(diǎn)評(píng) App 團(tuán)隊(duì)的平臺(tái)化實(shí)踐經(jīng)驗(yàn),學(xué)習(xí)和借鑒了許多優(yōu)秀的設(shè)計(jì)思想,但也由于盲目濫用踩過(guò)不少坑。

我們認(rèn)識(shí)到架構(gòu)的選擇正如其他技術(shù)問(wèn)題一樣,應(yīng)該是面向問(wèn)題的,而不是面向技術(shù)本身。

架構(gòu)的演進(jìn)必須在理論和實(shí)踐中交替前行,脫離了其中一個(gè)談?wù)摷軜?gòu),都將是個(gè)悲劇。

展望

平臺(tái)化之后,各業(yè)務(wù)團(tuán)隊(duì)的協(xié)作關(guān)系和開(kāi)發(fā)流程都發(fā)生了很大轉(zhuǎn)變。在如何提升平臺(tái)支持能力,如何保持架構(gòu)的穩(wěn)定性,如何使得各業(yè)務(wù)進(jìn)一步解耦等問(wèn)題上,我們又將面對(duì)新的問(wèn)題和挑戰(zhàn)。

其中有三個(gè)問(wèn)題是亟待我們解決的:

  • 要確保在長(zhǎng)期的業(yè)務(wù)迭代中架構(gòu)不被破壞,除了流程規(guī)范之外,還需要在本地編譯、遠(yuǎn)程提交、代碼合并、打包提測(cè)等各個(gè)階段建立更健全的檢查工具來(lái)約束,而目前這些工具鏈還不完善。
  • 插件化架構(gòu)是平臺(tái)型 App 集成的最好方式,不僅使得子業(yè)務(wù)具備動(dòng)態(tài)發(fā)布的能力,還可以解決令人頭疼的編譯速度問(wèn)題。目前美團(tuán)平臺(tái)已經(jīng)在部分業(yè)務(wù)上較好的實(shí)現(xiàn)了插件化集成,外賣(mài)正在跟進(jìn)。
  • 統(tǒng)一頁(yè)面級(jí)開(kāi)發(fā)的標(biāo)準(zhǔn)化框架,可以解決代碼的可維護(hù)性、可測(cè)試性,和更細(xì)粒度的可復(fù)用性,并且有利于各種自動(dòng)化方案的實(shí)施。目前我們正在部分業(yè)務(wù)嘗試,后續(xù)會(huì)持續(xù)推進(jìn)。

參考資料:

  • MVP + Clean Architecture
  • 58同城沈劍:好的架構(gòu)源于不停地衍變,而非設(shè)計(jì)
  • 每個(gè)架構(gòu)師都應(yīng)該研究下康威定理
  • 微服務(wù)架構(gòu)的理論基礎(chǔ) - 康威定律
  • 架構(gòu)的本質(zhì)是管理復(fù)雜性,微服務(wù)本身也是架構(gòu)演化的結(jié)果
  • 微信 Android 模塊化架構(gòu)重構(gòu)實(shí)踐
  • 配置構(gòu)建變體
  • 美團(tuán) App 插件化實(shí)踐

作者:吳凱、曉飛、海冰

吳凱,美團(tuán)點(diǎn)評(píng)技術(shù)專(zhuān)家。2016 年加入美團(tuán)點(diǎn)評(píng),目前負(fù)責(zé)外賣(mài)用戶(hù)端 Android 團(tuán)隊(duì),主要致力于外賣(mài) Android 平臺(tái)化業(yè)務(wù)支持和技術(shù)建設(shè)。

曉飛,美團(tuán)點(diǎn)評(píng)資深工程師。2015 年加入原美團(tuán),是外賣(mài) Android 的早期開(kāi)發(fā)者之一,目前作為外賣(mài) Android App 負(fù)責(zé)人,主要負(fù)責(zé)平臺(tái)和業(yè)務(wù)架構(gòu)。

海冰,美團(tuán)點(diǎn)評(píng)高級(jí)工程師。2015 年加入原美團(tuán),曾支持開(kāi)店寶等 B 端業(yè)務(wù),目前作為外賣(mài) Android App 主力開(kāi)發(fā),負(fù)責(zé)商家容器模塊,及平臺(tái)化相關(guān)推進(jìn)工作。

責(zé)任編輯:武曉燕 來(lái)源: 美團(tuán)點(diǎn)評(píng)技術(shù)團(tuán)隊(duì)
相關(guān)推薦

2016-11-27 20:43:26

云計(jì)算迭代

2022-04-29 09:10:00

算法人工智能技術(shù)

2022-08-09 09:18:47

優(yōu)化實(shí)踐

2022-03-25 10:47:59

架構(gòu)實(shí)踐美團(tuán)

2018-07-13 09:53:27

移動(dòng)應(yīng)用美團(tuán)代碼

2022-08-26 20:00:00

系統(tǒng)架構(gòu)

2018-12-07 12:54:22

App美團(tuán)外賣(mài)iOS客戶(hù)端

2022-08-25 22:24:19

架構(gòu)電商系統(tǒng)

2017-12-08 18:45:41

程序員外賣(mài)運(yùn)維

2017-06-01 10:52:35

互聯(lián)網(wǎng)

2017-07-03 15:32:49

數(shù)據(jù)庫(kù)MySQL架構(gòu)

2017-12-29 08:54:58

高可用數(shù)據(jù)庫(kù)架構(gòu)

2022-08-09 08:42:15

引擎方案

2019-08-23 13:10:39

美團(tuán)點(diǎn)評(píng)Kubernetes集群管理

2018-01-19 14:04:05

人工智能機(jī)器學(xué)習(xí)智能語(yǔ)音

2023-11-14 20:51:08

2018-06-01 10:08:00

DBA美團(tuán)SQL

2017-03-22 17:51:43

互聯(lián)網(wǎng)

2015-11-16 16:00:21

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)