這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
序言
隨著容器雲技術的不斷成熟,微服務架構也變得越來越火。在微服務架構下,我們將原本單一的應用按照功能邊界分解成一系列獨立、專註的微服務。每個微服務對應傳統應用中的一個組件,但是可以獨立編譯、部署和擴充。每個團隊可以根據自身服務的需求和行業發展的現狀,自由選擇最適合的技術棧,比如程式設計語言和資料庫。Golang語言這幾年風華正茂,不僅kubernetes、 openshif和docker等容器雲相關的開源項目的開發語言是Golang,而且很多對即時性要求不高的微服務團隊也選擇Golang作為主要開發語言。
在這個大背景下,筆者也開始了Golang之旅。Golang作為一門全新的靜態類型開發語言,與既有的開發語言相比簡潔、有趣卻又超級強大,具備眾多令人興奮不已的新特性,其中最令筆者興奮的兩個特性分別是:
- goroutine和channel
- interface
如果說goroutine和channel是支撐起Golang的並行存取模型的基石,讓Golang在如今叢集化與多核化的時代成為一道極為亮麗的風景,那麼interface是Golang整個類型系統的基石,讓Golang在基礎編程哲學的探索上達到前所未有的高度。
Golang的設計哲學之一是“少即是多”,沒有萬能的語言,保持簡單性的方法就是只提供一種方法做事情,把事情做到極致。在這個原則的指導下,Golang對於物件導向編程的支援非常簡潔而優雅:
- 簡潔之處在於,Golang並沒有沿襲傳統物件導向編程中的諸多概念,比如繼承、虛函數、建構函式和解構函式、隱藏的this指標等
- 優雅之處在於,Golang對物件導向編程的支援是語言類型系統中的天然組成部分,整個類型系統通過interface串聯,渾然一體。多數語言都提供interface,但它們的interface都不同於Golang的interface,Golang中的interface與其他語言最大的一點區別是它的非侵入性。
筆者將要開發的使用者故事涉及一系列演算法步驟,即多個網路平面操作的演算法架構是相同的,而具體步驟中的行為有些差異,於是就想到了一種設計模式——模板方法。與其他物件導向語言不同的是,Golang天生就是組合式設計。
本文嘗試用Golang實現一下模板方法,與讀者共同體驗一下組合式設計的魅力。
模板方法回顧
定義
模板方法模式(Template Method Pattern)是定義一個操作中的演算法的架構,而將一些步驟延遲到子類中,使得子類可以不改變一個演算法的架構就可重新定義該演算法的某些特定步驟。
模板方法的通用類圖如下所示:
template-method.png
雖然模板方法的通用類圖非常簡單,但它卻是一個應用非常廣泛的設計模式。
抽象模板
AbstractClass叫做抽象模板,它的方法分為兩類:
- 基本方法:也叫基本操作,是由子類實現的方法,並且在模板方法中被調用
- 模板方法:可以有一個或幾個,一般是一個演算法架構,實現對基本方法的調度,完成固定的邏輯。
下面以C++代碼為例:
//AbstractClass.hstruct AbstractClass{ virtual ~AbstractClass() = default; void templateMethod();private: virtual void doAnyThing() = 0; virtual void doSomeThing() = 0;};//AbstractClass.cppvoid AbstractClass::templateMethod(){ doAnyThing(); doSomeThing();}
具體模板
ConcreteClass1和ConcreteClass2屬於具體模板,實現父類所定義的一個或多個抽象方法,也就是父類定義的基本方法在子類中得以實現。
下面以C++代碼為例:
//ConcreteClass1.hstruct ConcreteClass1 : AbstractClass{private: virtual void doAnyThing() override; virtual void doSomeThing() override;};//ConcreteClass1.cppvoid ConcreteClass1::doAnyThing(){ ...}void ConcreteClass1::doSomeThing(){ ...}
注:ConcreteClass2和ConcreteClass2的代碼類似,我們不再贅述。
執行個體化模板方法
引入問題
假設我們有兩個資料庫,即Mysql和Oracle,使用者操作時有相同的演算法架構,而演算法步驟的具體行為不同,比如資料庫的串連介面和關閉介面實現不同。
我們假定使用者的輸入是三元組(table, key, value),期望資料庫有一個事務操作,核心演算法的架構為:
- 串連資料庫
- 查詢使用者輸入的三元組對應的記錄是否存在
- 如果不存在,則插入新紀錄
- 如果存在,則更新記錄
- 關閉資料庫
說明:我們同時假定資料庫有資料記錄的活性檢測功能,當一條資料記錄長期沒有被引用是,就將它從資料庫中刪掉,所以使用者不用關心記錄的刪除操作。
注:該問題是筆者自己杜撰的。
建模
根據Golang的interface特性建模後,模板方法的類圖如下:
template-method-golang.png
下面對該圖做如下說明:
- Golang的interface只包含純方法聲明,那麼模板方法不能在interface裡定義,所以必須增加一個類(struct),我們記作DbTrans。DbTrans通過成員變數持有了介面Db,該成員變數在運行時動態綁定具體的資料庫執行個體,我們用單向關聯表示這種關係。圖中DbTrans的Exec()方法用於實現演算法架構,對應模式中的抽象模板
- 在Golang中,一個類只要實現了一個介面要求的所有函數,我們就說這個類實現了該介面,否則就說這個類和該介面沒有關係。類和介面之間沒有強制的契約關係(繼承),通過組合和動態綁定的方式來實現繼承和多態,我們用虛線表示這種隱形的繼承關係。圖中MySql和Oracle分別實現了介面Db,對應模式中的具體模板
實現
Db
Db是核心要素interface,實現代碼為:
type Db interface { Connect() Close() InsertRecord(tableName, key, value string) GetRecord(tableName, key string) (err error, value string) UpdateRecord(tableName, key, value string)}
DbTrans
DbTrans是承載模板方法的struct,實現代碼為:
type DbTrans struct { Inst Db}func (this *DbTrans) Exec(tableName, key, value string) { if this.Inst == nil { return } this.Inst.Connect() if err, _ := this.Inst.GetRecord(tableName, key); err != nil { this.Inst.InsertRecord(tableName, key, value); } else { this.Inst.UpdateRecord(tableName, key, value) } this.Inst.Close()}
說明:該演算法架構和“引入問題”一節中給出的架構完全一樣
Mysql
MySql是承載具體模板方法的struct,代碼實現如下:
type Mysql struct {}func (_ *Mysql) Connect() { fmt.Println("mysql connect...")}func (_ *Mysql) Close() { fmt.Println("mysql close...\n")}func (_ *Mysql) InsertRecord(tableName, key, value string) { fmt.Printf("mysql tablename-%v insert record(%v, %v) succ!\n", tableName, key, value)}func (_ *Mysql) GetRecord(tableName, key string) (err error, value string) { i := rand.Intn(5) if i < 2 { fmt.Println("mysql", tableName, "table get record by", key, "failed!") return errors.New("record is not existed"), "nop" } fmt.Println("mysql", tableName, "table get record by", key, "succ!") return nil, "nop"}func (_ *Mysql) UpdateRecord(tableName, key, value string) { fmt.Printf("mysql tablename-%v update record(%v, %v) succ!\n", tableName, key, value)}
我們看看Mysql中的GetRecord方法實現:
- 用[0, 5]之間的隨機數是否小於2來類比是否尋找失敗
- 如果尋找失敗,就插入記錄
- 如果尋找成功,就更新記錄
Oracle
Oracle與Mysql類似,不再贅述。
Client
Client在main函數裡實現,代碼實現如下:
func main() { trans := new(DbTrans) trans.Inst = new(Mysql) trans.Exec("department", "cloudman", "a architect and be good at openstack") trans.Inst = new(Oracle) trans.Exec("department", "cloudman", "a architect and be good at openstack and like dancing")}
顯然,使用者分別觸發了兩個資料庫的事務操作,一個是mysql,一個是oracle,這兩個事務的演算法架構都是DbTrans。
日誌
運行模板方法的Golang代碼,日誌如下:
mysql connect...mysql department table get record by cloudman failed!mysql tablename-department insert record(cloudman, a architect and be good at openstack) succ!mysql close...oracle connect...oracle department table get record by cloudman succ!oracle tablename-department update record(cloudman, a architect and be good at openstack and like dancing) succ!oracle close...
小結
本文先回顧了模板方法的通用定義和實現,然後引入一個問題,通過建模和實現等步驟執行個體化了Golang版的模板方法,這對筆者即將開發的使用者故事有一定的價值,也希望對讀者深刻理解Golang的interface有一定的協助。