GoFrame 框架設(shè)計(jì)-模塊化設(shè)計(jì)

2023-05-24 21:08 更新

本章節(jié)我們先講一講在軟件設(shè)計(jì)中,模塊化的一些設(shè)計(jì)和復(fù)用原則,然后再介紹?GoFrame?框架的模塊化設(shè)計(jì),以便于大家更好地了解?GoFrame?框架模塊化設(shè)計(jì)的思想。

一、什么是模塊

模塊也稱作組件,是軟件系統(tǒng)中可復(fù)用的功能邏輯封裝單位。在不同的軟件架構(gòu)層次,模塊的概念會(huì)有些不太一樣。在開發(fā)框架層面,模塊是某一類功能邏輯的最小封裝單位。在Golang代碼層面中,我們也可以將package稱作模塊。

二、模塊化的目標(biāo)

軟件進(jìn)行模塊化設(shè)計(jì)的目的,是為了使得軟件功能邏輯盡可能的解耦和復(fù)用,終極目標(biāo)也是為了保證軟件開發(fā)維護(hù)的效率和質(zhì)量。

三、模塊復(fù)用原則

  • ?REP復(fù)用/發(fā)布等同原則

復(fù)用/發(fā)布等同原則(?Release/Reuse Equivalency Principle?):軟件復(fù)用的最小粒度應(yīng)等同于其發(fā)布的最小粒度。

直白地說,就是要復(fù)用一段代碼就把它抽成模塊。

  • ?CCP共同閉包原則

共同閉包原則(?Common Closure Principle?):為了相同目的而同時(shí)修改的類,應(yīng)該放在同一個(gè)模塊中。

對大部分應(yīng)用程序而言,可維護(hù)性的重要性遠(yuǎn)遠(yuǎn)大于可復(fù)用性,由同一個(gè)原因引起的代碼修改,最好在同一個(gè)模塊中,如果分散在多個(gè)模塊中,那么開發(fā)、提交、部署的成本都會(huì)上升。

  • ?CRP共同復(fù)用原則

共同復(fù)用原則(?Common Reuse Principle?):不要強(qiáng)迫一個(gè)模塊依賴它不需要的東西。

相信你一定有這種經(jīng)歷,集成了模塊A,但模塊A依賴了模塊B、C。即使模塊B、C 你完全用不到,也不得不集成進(jìn)來。這是因?yàn)槟阒挥玫搅四KA的部分能力,模塊A中額外的能力帶來了額外的依賴。如果遵循共同復(fù)用原則,你需要把A拆分,只保留你要用的部分。

  • 復(fù)用原則競爭關(guān)系

?REP?、?CCP?、?CRP三個(gè)原則之間存在彼此競爭的關(guān)系。?REP和 ?CCP是黏合性原則,它們會(huì)讓模塊變得更大,而 ?CRP原則是排除性原則,它會(huì)讓模塊變小。遵守?REP?、?CCP而忽略 ?CRP,就會(huì)依賴了太多沒有用到的模塊和類,而這些模塊或類的變動(dòng)會(huì)導(dǎo)致你自己的模塊進(jìn)行太多不必要的發(fā)布;遵守 ?REP、?CRP而忽略 ?CCP?,因?yàn)槟K拆分的太細(xì)了,一個(gè)需求變更可能要改n個(gè)模塊,帶來的成本也是巨大的。

2652711029-5d007c0c1f843_articlex

優(yōu)秀的架構(gòu)師應(yīng)該能在上述三角形張力區(qū)域中定位一個(gè)最適合目前研發(fā)團(tuán)隊(duì)狀態(tài)的位置,例如在項(xiàng)目早期,?CCP?比?REP?更重要,隨著項(xiàng)目的發(fā)展,這個(gè)最合適的位置也要不停調(diào)整。

四、框架模塊設(shè)計(jì)

經(jīng)過前面關(guān)于模塊設(shè)計(jì)原則和復(fù)用原則的介紹,我們應(yīng)該對模塊開發(fā)和管理這塊的原則有了大概的了解,那么我們接著介紹框架的模塊化設(shè)計(jì)就比較容易理解了。

單倉庫包設(shè)計(jì)

根據(jù)?REP?原則我們了解到,一個(gè)可復(fù)用的模塊是支持獨(dú)立版本管理的,單倉庫包設(shè)計(jì)也正是如此。Golang中很多這樣的單倉庫包,一個(gè)包就是一個(gè)獨(dú)立的模塊。單倉庫包根據(jù)?CRP?原則可以再進(jìn)一步的細(xì)化解耦拆分。我們來舉個(gè)例子,在開發(fā)復(fù)雜的業(yè)務(wù)項(xiàng)目場景下,常見的包依賴情況,類似于這樣的:

module business

go 1.16

require (
    business.com/golang/strings v1.0.0
    business.com/golang/config v1.15.0
    business.com/golang/container v1.1.0
    business.com/golang/encoding v1.2.0
    business.com/golang/files v1.2.1
    business.com/golang/cache v1.7.3
    business.com/framework/utils v1.30.1
    github.com/pkg/errors v0.9.0
    github.com/goorm/orm v1.2.1
    github.com/goredis/redis v1.7.4
    github.com/gokafka/kafka v0.1.0
    github.com/gometrics/metrics v0.3.5
    github.com/gotracing/tracing v0.8.2
    github.com/gohttp/http v1.18.1
    github.com/google/grpc v1.16.1
    github.com/smith/env v1.0.2
    github.com/htbj/command v1.1.1
    github.com/kmlevel1/pool v1.1.4
    github.com/anolog/logging v1.16.2
    github.com/bgses123/session v1.5.1
    github.com/gomytmp/template v1.3.4
    github.com/govalidation/validate v1.19.2
    github.com/yetme1/goi18n v0.10.0
    github.com/convman/convert v1.20.0
    github.com/google/uuid v1.1.2
    // ...
)

示例中的模塊依賴,都是一些通用模塊,大部分業(yè)務(wù)項(xiàng)目都會(huì)涉及到。模塊地址是便于演示而寫的隨意地址,并不一定真實(shí)存在。

使用Golang開發(fā)過復(fù)雜一點(diǎn)的業(yè)務(wù)項(xiàng)目的小伙伴們,對于這樣的場景大家一定不會(huì)陌生。一個(gè)正常的軟件企業(yè),往往至少有數(shù)百個(gè)這樣的項(xiàng)目,真實(shí)的模塊依賴關(guān)系比這里的例子更加復(fù)雜。在Golang項(xiàng)目開發(fā)中,對于模塊依賴的維護(hù)性挑戰(zhàn)是比較大的,我們往往會(huì)遇到一些痛點(diǎn),主要的幾點(diǎn):

  • 實(shí)現(xiàn)相同功能邏輯的模塊較多,選擇成本增加
  • 項(xiàng)目依賴的模塊過多,項(xiàng)目整體的穩(wěn)定性會(huì)受到影響
  • 項(xiàng)目依賴的模塊過多,項(xiàng)目無從下手是否應(yīng)當(dāng)升級(jí)這些模塊版本
  • 模塊分散設(shè)計(jì),不成體系,難以統(tǒng)一。

現(xiàn)身說法舉例。

本廠的自研模塊有數(shù)十個(gè),這些模塊已經(jīng)被頻繁使用遍布到數(shù)百個(gè)業(yè)務(wù)項(xiàng)目中。有一次,我們提交了對幾個(gè)模塊的?bug fix?,其中有兩個(gè)還是比較重要的?bug?,緊接著,我們要求所有業(yè)務(wù)項(xiàng)目全部升級(jí)一下對應(yīng)模塊的版本號(hào),并且這些版本號(hào)填寫得務(wù)必小心。當(dāng)然,這肯定也不是唯一的一次,隨后相同的場景各位同學(xué)可以自行腦補(bǔ)。

我們也可以選擇,不去主動(dòng)推進(jìn)所有業(yè)務(wù)項(xiàng)目升級(jí)模塊,只要項(xiàng)目還沒有觸發(fā)這些?bug?,那么就等著業(yè)務(wù)項(xiàng)目踩到了坑再由項(xiàng)目組自行去升級(jí)。領(lǐng)導(dǎo)如果聽到這種解決方案......各位同學(xué)再自行腦補(bǔ)一下和諧的場景。

其實(shí)這種問題主要的原因,還是來源于模塊的不穩(wěn)定,模塊也是需要不停迭代改進(jìn)的。項(xiàng)目使用到這些模塊,那么就與這些模塊建立了耦合關(guān)系,耦合模塊的變化,必然會(huì)影響到依賴的相關(guān)項(xiàng)目。越底層的基礎(chǔ)模塊,頂層模塊則對其依賴的越多,影響面也就越大。那是不是只要模塊穩(wěn)定了,就不會(huì)存在這樣的問題了呢?風(fēng)險(xiǎn)依舊是存在的。Golang標(biāo)準(zhǔn)庫大家覺得算穩(wěn)定吧,但是它也是在不斷的迭代改進(jìn)過程中,也是不斷有bug出現(xiàn),只是大家幸運(yùn)沒踩上去而已,風(fēng)險(xiǎn)相對較低。

好的軟件設(shè)計(jì),并不是一成不變,而是能夠做到快速響應(yīng)變化,根據(jù)變化快速改進(jìn)完善。模塊的設(shè)計(jì)和管理,亦是如此。尋求能夠快速改進(jìn)模塊邏輯、有效維護(hù)模塊依賴的方案,比編寫更加穩(wěn)定的功能模塊,更加高效和務(wù)實(shí)。

模塊聚合設(shè)計(jì)

?GoFrame?的模塊化管理思想更偏重于?CCP?原則,看重可維護(hù)性比可復(fù)用性更多。由于?GoFrame?是基于開發(fā)框架層面的出發(fā)點(diǎn)考慮,因此整體框架的設(shè)計(jì)不是單點(diǎn)設(shè)計(jì)的,而是自頂向下設(shè)計(jì)的。前面有提到,越底層的基礎(chǔ)模塊,頂層模塊則對其依賴的越多,影響面也就越大。因此,框架將一些通用性的核心模塊進(jìn)行統(tǒng)一維護(hù),這樣做的目的是使得這些模塊共同形成閉包,保證基礎(chǔ)模塊的穩(wěn)定性,并通過統(tǒng)一的版本管理,提高開發(fā)效率和可維護(hù)性,降低接入和維護(hù)成本。

站在?GoFrame?框架模塊化設(shè)計(jì)的角度,前面例子中的依賴情況應(yīng)當(dāng)變成以下的樣子:

module business

go 1.16

require (
    github.com/gogf/gf v1.16.0
    github.com/goorm/orm v1.15.1     
    github.com/goredis/redis v1.7.4
    github.com/gokafka/kafka v0.1.0
    github.com/google/grpc v1.16.1
    // ...
)

?GoFrame?只維護(hù)一些通用性的核心模塊,其他非通用核心模塊或者穩(wěn)定性較高的模塊,依舊建議使用單倉庫包的形式進(jìn)行依賴引入,正如?REP?和?CRP?模塊復(fù)用原則倡導(dǎo)的那樣。在這種設(shè)計(jì)模式下:

  • 框架核心維護(hù)較全面的通用基礎(chǔ)模塊,降低基礎(chǔ)模塊選擇成本
  • 我們只需要維護(hù)一個(gè)統(tǒng)一的框架版本,而不是數(shù)十個(gè)模塊版本
  • 我們只需要了解一個(gè)框架的內(nèi)容變化,而不是數(shù)十個(gè)模塊的內(nèi)容變化
  • 升級(jí)的時(shí)候只需要升級(jí)一個(gè)框架版本,而不是數(shù)十個(gè)模塊版本的升級(jí)
  • 減輕開發(fā)人員的心智負(fù)擔(dān),提高模塊可維護(hù)性,更容易保證各業(yè)務(wù)項(xiàng)目的模塊版本一致性

五、常見問題解答

雖然每一個(gè)模塊都按照低耦合設(shè)計(jì),模塊可以選擇性引入,但在使用時(shí)也得全量下載完整框架代碼

文件層面的源文件下載與模塊之間的邏輯耦合沒有直接關(guān)系。需要注意的是,編譯型語言和解釋型語言的模塊管理邏輯不太一樣。

image2021-3-25_17-10-44

  • 編譯型語言:(以靜態(tài)編譯為例)往往以?main?包為入口,編譯器會(huì)自動(dòng)分析源碼并將所有邏輯依賴模塊中對應(yīng)的資源進(jìn)行編譯處理,最終生成為靜態(tài)二進(jìn)制文件進(jìn)行發(fā)布,自身源文件以及依賴模塊(邏輯依賴)的源文件只在編譯階段使用,源碼文件并不會(huì)直接用于發(fā)布,如:C/C++、Golang、Rust等。
  • 解釋型語言:往往會(huì)將自身源文件(或中間碼)以及依賴模塊的源文件(或中間碼)全部進(jìn)行打包發(fā)布,例如:PHP、Java、NodeJS、Python等。這個(gè)時(shí)候,依賴模塊的源碼大小對于項(xiàng)目發(fā)布來說影響會(huì)比較大。并且,打包時(shí)候的模塊依賴處理并不會(huì)檢查"邏輯依賴",只要依賴配置文件中存在指定模塊,那么該模塊都會(huì)被共同打包發(fā)布。假如模塊中有10萬個(gè)函數(shù),即使其中只有一個(gè)函數(shù)被使用到,該模塊所有函數(shù)將被共同打包發(fā)布。因?yàn)榻忉屝驼Z言在代碼發(fā)布前并沒有"編譯-匯編-鏈接"等階段,只能在運(yùn)行時(shí)對源碼及模塊依賴做完整解析處理。特別是PHP/Java轉(zhuǎn)Go的同學(xué),這一塊的思維需要轉(zhuǎn)變適應(yīng)。

框架中任一模塊的版本變更都會(huì)引起框架版本的發(fā)布,框架的發(fā)布頻次是否會(huì)變高

最主要的一點(diǎn),框架的模塊設(shè)計(jì)也會(huì)充分考慮穩(wěn)定性因素,僅會(huì)將一些通用性的核心模塊按照?CCP?進(jìn)行管理,并不會(huì)包含特定業(yè)務(wù)的邏輯封裝,因?yàn)樯婕暗教囟I(yè)務(wù)的功能邏輯實(shí)現(xiàn)將會(huì)為框架模塊帶來更多的不穩(wěn)定變化。

在保證一定的穩(wěn)定性前提下,模塊的版本發(fā)布按照框架統(tǒng)一的迭代開發(fā)計(jì)劃進(jìn)行,除了必要的?hot fix?之外,版本發(fā)布設(shè)置有固定的時(shí)間窗口,以保證框架核心的穩(wěn)定性。因此,框架通過模塊聚合的方式進(jìn)行版本管理,不僅沒有增加框架的版本發(fā)布頻次,反而降低了框架的版本發(fā)布頻次,使得框架中的模塊版本更加穩(wěn)定。

框架聚合并維護(hù)通用性的核心模塊,通用性的核心模塊定義是什么

首先,它們是基礎(chǔ)模塊,往往位于模塊依賴鏈的最底層,這部分的模塊變化對項(xiàng)目穩(wěn)定性影響最大。

其次,絕大部分項(xiàng)目(二八定律來講為80%以上)都會(huì)依賴的通用性基礎(chǔ)模塊,可以稱作核心模塊。

最后,這部分模塊不包含具體業(yè)務(wù)的封裝實(shí)現(xiàn)。例如:微信公眾號(hào)/小程序、CMS/CRM、區(qū)塊鏈等相關(guān)模塊都是具體業(yè)務(wù)實(shí)現(xiàn)封裝。

關(guān)于模塊通用性的評(píng)估無法完全準(zhǔn)確,框架為保證核心精簡會(huì)盡可能持保守態(tài)度,并且會(huì)根據(jù)實(shí)際情況在未來的迭代中逐步做調(diào)整。

以下為可供參考的模塊分層:

image2021-3-8_22-51-45

  • 業(yè)務(wù)實(shí)現(xiàn)模塊:特定業(yè)務(wù)項(xiàng)目邏輯實(shí)現(xiàn),這里包含業(yè)務(wù)項(xiàng)目進(jìn)一步的代碼分層。
  • 通用業(yè)務(wù)模塊:可復(fù)用的業(yè)務(wù)邏輯封裝,例如微信公眾號(hào)/小程序、CMS/CRM、區(qū)塊鏈等相關(guān)業(yè)務(wù)邏輯封裝模塊。
  • 通用基礎(chǔ)模塊:標(biāo)準(zhǔn)庫不提供或者基于標(biāo)準(zhǔn)庫封裝擴(kuò)展的基礎(chǔ)模塊,例如:配置、校驗(yàn)、緩存、ORM、I18N等等。
  • 標(biāo)準(zhǔn)基礎(chǔ)模塊:Golang標(biāo)準(zhǔn)庫。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)