導讀:COLA 的主要目的是為應用架構提供一套簡單的可以復制、可以理解、可以落地、可以控制復雜性的”指導和約束"。在實踐中作者發(fā)現 COLA 在簡潔性上仍有不足,因此給 COLA 做了一次“升級”,在這次升級中,沒有增加任何新的功能,而是盡量多刪減了一些概念和功能阿里巴巴中文網站的架構和基本功能,讓 COLA 更簡潔有效。
最近,同事告訴我,COLA 作為應用架構,已經被選入阿里云的 Java 應用初始化的應用架構選項之一。
This is ,于是,在這個里程碑節(jié)點上,我開始回過頭來,重新審視 COLA 一路走來的得與失。
COLA 作為一種架構思想無疑是成功的。但是作為框架,個人感覺有點雞肋之嫌。特別是在簡潔性上做的不好,感覺做了不少畫蛇添足的事情。
試想一下,有些功能我作為作者都很少去使用,我實在想不到,它為什么還有存在的理由。
基于上面的思考,我做了這一次 COLA 2.0 到 COLA 3.0 的升級。在本次升級中,我沒有增加任何新的功能,而是盡量多刪減了一些概念和功能。讓 COLA 可以更加純粹的 focus 在應用架構上,而不是框架支持和架構約束上。
支持我做這些決策的背后原因只有一個——奧卡姆剃刀原理。
奧卡姆剃刀原理
奧卡姆剃刀原理,是指如無必要,勿增實體( not be ),即“簡單有效原理”。正如奧卡姆在《箴言書注》2 卷 15 題說“切勿浪費較多東西去做,用較少的東西,同樣可以做好的事情。”
在具體的應用過程中,我們可以遵循以下原則去做事情:
“如果同一個現象有 n 種理論,最簡單的那個便是最正確的。能用 n 做好事情,那就不要有第 n+1 個動作。”
比如,《皇帝的新衣》的皇帝到底穿沒穿衣服呢?如果你在現場,你很有可能就是大臣之一。
如果懂得了奧卡姆剃刀原理,可以用邏輯手段,判斷誰是真理。
同樣看見光身子的皇帝,第二種解釋簡單明了。而第一種解釋,可能正因為它是錯誤的,就需要更多假設來補救漏洞,就像說謊圓謊一樣。
真相不需要偽裝掩飾,簡單明了。
再比如,地心說和日心說,托勒密的地心說模型是一個本輪均輪模型。人們可以按照這個模型,定量計算行星的運動,據此推測行星所在的位置。
到了中世紀后期隨著觀察儀器的不斷改進,人們能夠更加精確地測量出行星的位置和運動,觀測到行星實際位置與這個模型的計算結果存在偏差,一開始還能勉強應付,后來小本輪增加到八十多個,卻仍然不能精確地計算出行星的準確位置。
1543 年,波蘭天文學家哥白尼在臨終時發(fā)表了一部具有歷史意義的著作——《天體運行論》。這個理論體系提出了一個明確的觀點:太陽是宇宙的中心,一切行星都在圍繞太陽旋轉。該理論認為,地球也是行星之一,它一方面像陀螺一樣自轉,一方面又和其他行星一樣圍繞太陽轉動。
哥白尼的計算不僅結構嚴謹,而且計算簡單,與已經加到八十余個圈的地心說相比,哥白尼的計算與實際觀測資料能更好地吻合。因此,地心說最終被日心說所取代。
設計中的彎彎繞
深入考察一下,我們系統中,類似于“地心說”這樣的彎彎繞的設計,實在是不在少數。
從系統架構的角度看,有些彎彎繞是因為系統邊界劃分不合理,導致職責不清,依賴混亂。
從應用架構的角度看,有些彎彎繞是因為過度設計阿里巴巴中文網站的架構和基本功能,為了追求所謂的靈活性和可擴展性,使用了不恰當的設計。導致本來可以直觀呈現的代碼邏輯,被各種包裝,各種隱藏,各種轉發(fā).... 無形中極大的阻礙了代碼的可讀性和可理解性,增加了維護成本。
舉個例子,我看過無數的業(yè)務系統,喜歡拿業(yè)務流程編排說事情。因此,在業(yè)務系統中,可以看到各種五花八門的“彎彎繞設計”。
比如,在一個業(yè)務系統中,我看到了如下的 設計。這個設計的本質是說把一個復雜的業(yè)務操作進行結構化拆解為多個小的處理單元。
拆解是正確的,但是這種處理方式顯然不夠“奧卡姆”(關于更多結構化分解的內容,可以看我的另一篇文章:如何寫復雜業(yè)務代碼?)。作為維護人員,進入“入口函數”后,還要去查數據庫,然后才能知道哪些組件被調用了,太繞了,不夠直觀,也不簡潔。
同樣的邏輯,按照下面的方式寫不香嗎?
public class CreateCSPUExecutor {
@Resource
private InitContextStep initContextStep;
@Resource
private CheckRequiredParamStep checkRequiredParamStep;
@Resource
private CheckUnitStep checkUnitStep;
@Resource

private CheckExpiringDateStep checkExpiringDateStep;
@Resource
private CheckBarCodeStep checkBarCodeStep;
@Resource
private CheckBarCodeImgStep checkBarCodeImgStep;
@Resource
private CheckBrandCategoryStep checkBrandCategoryStep;
@Resource
private CheckProductDetailStep checkProductDetailStep;
@Resource
private CheckSpecImgStep checkSpecImgStep;
@Resource
private CreateCSPUStep createCSPUStep;
@Resource
private CreateCSPULogStep createCSPULogStep;
@Resource
private SendCSPUCreatedEventStep sendCSPUCreatedEventStep;
public Long create(MyCspuSaveParam myCspuSaveParam){
SaveCSPUContext context = initContextStep.initContext(myCspuSaveParam);
checkRequiredParamStep.check(context);
checkUnitStep.check(context);
checkExpiringDateStep.check(context);
checkBarCodeStep.check(context);
checkBarCodeImgStep.check(context);
checkBrandCategoryStep.check(context);
checkProductDetailStep.check(context);
checkSpecImgStep.check(context);
createCSPUStep.create(context);
createCSPULogStep.log(context);
sendCSPUCreatedEventStep.sendEvent(context);
return context.getCspu().getId();
}
}
public class CreateCSPUExecutor {
@Resource
private InitContextStep initContextStep;
@Resource

private CheckRequiredParamStep checkRequiredParamStep;
@Resource
private CheckUnitStep checkUnitStep;
@Resource
private CheckExpiringDateStep checkExpiringDateStep;
@Resource
private CheckBarCodeStep checkBarCodeStep;
@Resource
private CheckBarCodeImgStep checkBarCodeImgStep;
@Resource
private CheckBrandCategoryStep checkBrandCategoryStep;
@Resource
private CheckProductDetailStep checkProductDetailStep;
@Resource
private CheckSpecImgStep checkSpecImgStep;
@Resource
private CreateCSPUStep createCSPUStep;
@Resource
private CreateCSPULogStep createCSPULogStep;
@Resource
private SendCSPUCreatedEventStep sendCSPUCreatedEventStep;
public Long create(MyCspuSaveParam myCspuSaveParam){
SaveCSPUContext context = initContextStep.initContext(myCspuSaveParam);
checkRequiredParamStep.check(context);

checkUnitStep.check(context);
checkExpiringDateStep.check(context);
checkBarCodeStep.check(context);
checkBarCodeImgStep.check(context);
checkBrandCategoryStep.check(context);
checkProductDetailStep.check(context);
checkSpecImgStep.check(context);
createCSPUStep.create(context);
createCSPULogStep.log(context);
sendCSPUCreatedEventStep.sendEvent(context);
return context.getCspu().getId();
}
}
這種寫法簡單直觀,易維護,與前一種方式相比,具有同樣的組件復用性。符合奧卡姆剃刀的精神,相比較而言,前面那種彎彎繞設計,雖然看起來有點設計感,帶來了一點點 OCP 的好處。但是無端增加了理解和認知成本,孰優(yōu)孰劣,不難分辨。
COLA 3.0 升級
做了這么長的鋪墊,終于到了批斗 COLA 中“彎彎繞設計”的時候了。
1. 去掉
在 COLA 的初始階段,因為受到 CQRS 的影響,于是想到了使用命令模式來處理用戶請求。設計的初衷是想通過框架,一方面強制約束 和 Query 的處理方式,另一方面把 里面的邏輯,強制拆分到 中去,防止 膨脹過快。
和上面介紹過的 設計類似,這種設計有點繞,不夠直觀,如下所示:
public class MetricsServiceImpl implements MetricsServiceI{
@Autowired
private CommandBusI commandBus;
@Override
public Response addATAMetric(ATAMetricAddCmd cmd) {
return commandBus.send(cmd);
}

@Override
public Response addSharingMetric(SharingMetricAddCmd cmd) {
return commandBus.send(cmd);
}
@Override
public Response addPatentMetric(PatentMetricAddCmd cmd) {
return commandBus.send(cmd);
}
@Override
public Response addPaperMetric(PaperMetricAddCmd cmd) {
return commandBus.send(cmd);
}
}
看起來還挺干凈的,可是 到底是被哪個 處理的呢,不直觀。我還要去理解 ,以及 是如何注冊 的。無形中增加了認知成本,不好。
既然這樣,為何不用奧卡姆剃刀把這個 剔除呢。如下所示,去除 之后,代碼是不是直觀了很多,唯一的損失是我們會失去框架層面提供的 功能,然而, 正是我下一個要動刀的地方。
public class MetricsServiceImpl implements MetricsServiceI{
@Resource
private ATAMetricAddCmdExe ataMetricAddCmdExe;
@Resource
private SharingMetricAddCmdExe sharingMetricAddCmdExe;
@Resource
private PatentMetricAddCmdExe patentMetricAddCmdExe;
@Resource
private PaperMetricAddCmdExe paperMetricAddCmdExe;
@Override
public Response addATAMetric(ATAMetricAddCmd cmd) {
return ataMetricAddCmdExe.execute(cmd);
}
@Override
public Response addSharingMetric(SharingMetricAddCmd cmd) {
return sharingMetricAddCmdExe.execute(cmd);
}

@Override
public Response addPatentMetric(PatentMetricAddCmd cmd) {
return patentMetricAddCmdExe.execute(cmd);
}
@Override
public Response addPaperMetric(PaperMetricAddCmd cmd) {
return paperMetricAddCmdExe.execute(cmd);
}
}
2. 去掉
當時設計 ,是因為有 作為基礎,為了更好的利用命令模式帶來的好處,便添加了 功能。其本質是一個 AOP 處理。
鑒于 的 AOP 功能已經很完善了,這個設計也是有點雞肋。事實證明,大家在使用 COLA 框架的時候,很少會使用 ,包括我自己也是一樣。既然如此,剔除也罷。
3. 去掉 、、
關于命名的重要性,這里就不贅述了。當時想著是否能從框架層面,規(guī)范一下一些常用功能的命名。但是在實際使用中,發(fā)現這個想法也是有些過于理想化了。
我記得,在團隊實踐 COLA 的初期,還經常為什么是 (轉換器),什么是 (組裝器)的事情,爭論不休。
后面我仔細想了想,命名雖然很重要,但其作用域最多也就是一個團隊規(guī)范,你校驗器是叫 還是 并沒有什么本質區(qū)別,團隊自己定義就好了。嘗試從框架層面去解決團隊約定問題,其效果不會太好,因此也果斷揮刀剔除。
4. 類掃描優(yōu)化
業(yè)務身份和擴展點的思想,是 TMF 的核心理念,也是阿里業(yè)務中臺的進行多業(yè)務支持的核心方法論。
COLA 致力于提供一種輕量級的擴展實現方式,因此該功能在奧卡姆的屠刀下得以保存。因為 COLA 的擴展點設計是借鑒了中臺的 TMF,因此在前面的設計中,其類掃描方案是直接照搬 TMF 的做法。
實際上,TMF 的類掃描方案對 COLA 來說有點多余。因為 COLA 本身就是架設在 的基礎之上,而 又是建立在類掃描的基礎之上。因此,我們完全可以復用 的類掃描,沒必要自己寫一套。
在原生的 中,至少有 3 種方式可以獲取到用戶自定義 的 Bean,最簡潔的是通過 .on 方法,或者使用 der 進行掃包。
在這次改版中,我選用的是 on 方法,主要是為了獲取 @ 的 Bean,用來實現擴展點功能,廢棄了原來的 TMF 類掃描實現。
總結
觸發(fā)這次升級的動機,主要是因為,自己在實踐 COLA 的過程中,的確發(fā)現有些華而不實的功能。在 COLA 作為阿里云的基礎應用架構,其影響力越來越大的時候,我有責任給到大家一個正確的引導——去偽存真,簡潔有效,而不是引入更多的復雜度。
實際上,COLA 是由兩部分組成的:
一方面 COLA 是一種架構思想,是整合了洋蔥圈架構、適配器架構、DDD、整潔架構、TMF 等架構思想的一種應用架構。
在這次升級中,架構思想部分基本沒有變化,唯一一點是因為去除了 概念,因此 CQRS 也成了可選項,而不再是一種強要求。
另一方面 COLA 也是框架組件,通過這次升級,我使用奧卡姆剃刀砍掉了絕大部分的組件能力,僅僅保留了擴展點功能。其用意是不希望 COLA 作為框架給到應用開發(fā)者太多的約束,這不符合簡單有效的風格。
所以,總結下來,與其說這是一次升級,不如說它是功能“降級”,是在做減法。
但我相信,減法可以讓 COLA 更加符合奧卡姆精神,幫助 COLA 輕裝上陣,走的更遠。
COLA 開源地址://COLA
阿里云 JAVA 應用腳手架
是基于 - 實現的工程腳手架生成平臺,開發(fā)者們只需要添加一些注解和少量配置,就可以快速搭建分布式應用系統,它使用更親切的中文,也不會有網絡延遲問題,最重要的是提供更多本地化的組件依賴。
點擊鏈接,立即體驗阿里云 JAVA 應用腳手架:/?=
“阿里巴巴云原生關注微服務、、容器、 Mesh 等技術領域、聚焦云原生流行技術趨勢、云原生大規(guī)模的落地實踐,做最懂云原生開發(fā)者的公眾號。”