第二條原則: 偏好組合,正交解耦
當(dāng)我們有必要采用另外一種方式處理數(shù)據(jù)時(shí),我們應(yīng)該有一些耦合程序的方式,就像花園里將澆水的軟管通過預(yù)置的螺絲扣擰入另一段那樣,這也是 Unix IO 采用的方式。- 道格·麥克羅伊,Unix 管道的發(fā)明者(1964)
C++、Java 等主流面向?qū)ο螅ㄒ韵潞?jiǎn)稱 OO)語言通過龐大的、自上而下的類型體系、繼承、顯式接口實(shí)現(xiàn)等機(jī)制將程序的各個(gè)部分耦合起來,但在 Go 語言中我們找不到經(jīng)典 OO 的語法元素、類型體系和繼承機(jī)制,或者說 Go 語言本質(zhì)上就不屬于經(jīng)典 OO 語言范疇。針對(duì)這種情況,很多人會(huì)問:那 Go 語言是如何將程序的各個(gè)部分有機(jī)地耦合在一起的呢?就像上面引述的道格.麥克羅伊的那句話中的澆水軟管那樣,Go 語言遵從的設(shè)計(jì)哲學(xué)也是組合。
在詮釋組合之前,我們可以先來了解一下 Go 在語法元素設(shè)計(jì)時(shí)是如何為組合哲學(xué)的應(yīng)用奠定基礎(chǔ)的。
在 Go 語言設(shè)計(jì)層面,Go 設(shè)計(jì)者為 gopher 們提供了正交的語法元素供后續(xù)組合使用,包括:
- Go 語言無類型體系(type hierarchy),類型之間是獨(dú)立的,沒有子類型的概念;
- 每個(gè)類型都可以有自己的方法集合,類型定義與方法實(shí)現(xiàn)是正交獨(dú)立的;
- 接口(interface)與其實(shí)現(xiàn)之間"隱式關(guān)聯(lián)";
- 包(package)之間是相對(duì)獨(dú)立的,沒有子包的概念。
我們看到無論是包、接口還是一個(gè)個(gè)具體的類型定義(包括類型的方法集合),Go 語言為我們呈現(xiàn)了這樣的一幅圖景:一座座沒有關(guān)聯(lián)的“孤島”,但每個(gè)島內(nèi)又都很精彩?,F(xiàn)在擺在面前的工作就是在這些孤島之間以最適當(dāng)?shù)姆绞浇㈥P(guān)聯(lián)(耦合),形成一個(gè)"整體"。Go 采用了組合的方式,也是唯一的方式。
Go 語言提供了的最為直觀的組合的語法元素就是type embedding
,即類型嵌入。通過類型嵌入,我們可以將已經(jīng)實(shí)現(xiàn)的功能嵌入到新類型中,以快速滿足新類型的功能需求,這種方式有些類似經(jīng)典 OO 的“繼承”,但在原理上與經(jīng)典 OO 的繼承完全不同。這是一種 Go 精心設(shè)計(jì)的“語法糖”,被嵌入的類型和新類型兩者之間沒有任何關(guān)系,甚至相互完全不知道對(duì)方的存在,更沒有經(jīng)典 OO 那種父類、子類的關(guān)系以及向上、向下轉(zhuǎn)型(type casting)。通過新類型實(shí)例調(diào)用方法時(shí),方法的匹配取決于方法名字,而不是類型。這種組合方式,我稱之為“垂直組合”,即通過類型嵌入,快速讓一個(gè)新類型“復(fù)用”其他類型已經(jīng)實(shí)現(xiàn)的能力,實(shí)現(xiàn)功能的垂直擴(kuò)展。
下面是一個(gè)類型嵌入的例子:
// Go標(biāo)準(zhǔn)庫:sync/pool.go
type poolLocal struct {
private interface{} // Can be used only by the respective P.
shared []interface{} // Can be used by any P.
Mutex // Protects shared.
pad [128]byte // Prevents false sharing.
}
我們?cè)?poolLocal 這個(gè) struct 中嵌入類型 Mutex,被嵌入的 Mutex 類型的方法集合會(huì)被提升到外面的類型中。比如,這里的 poolLocal 將擁有 Mutex 類型的 Lock 和 Unlock 方法。實(shí)際調(diào)用時(shí),方法調(diào)用實(shí)際會(huì)被傳給 poolLocal 中的 Mutex 實(shí)例。
我們?cè)跇?biāo)準(zhǔn)庫中還經(jīng)??吹筋愃迫缦碌?interface 類型嵌入的代碼:
type ReadWriter interface {
Reader
Writer
}
通過在 interface 中嵌入 interface type,實(shí)現(xiàn)接口行為的聚合,組成大接口,這種方式在標(biāo)準(zhǔn)庫中尤為常用,并且已經(jīng)成為了 Go 語言的一種常見的慣用法。
interface 是 Go 語言中真正的魔法,是 Go 語言的一個(gè)創(chuàng)新設(shè)計(jì),它只是方法集合,并且它與實(shí)現(xiàn)者之間的關(guān)系是隱式的,它讓程序內(nèi)部各部分之間的耦合降至最低,同時(shí)它也是連接程序各個(gè)部分之間“紐帶”。隱式的 interface 實(shí)現(xiàn)會(huì)不經(jīng)意間滿足:依賴抽象、里氏替換、接口隔離等原則,這在其他語言中是需要很"刻意"的設(shè)計(jì)謀劃才能實(shí)現(xiàn)的,但在 Go interface 來看,一切卻是自然而然的。
通過 interface 將程序內(nèi)部各個(gè)部分組合在一起的方法,我這里稱之為水平組合。水平組合的“模式”很多,比如:一種常見方法就是:通過接受 interface 類型參數(shù)的普通函數(shù)進(jìn)行組合,例如下面代碼。
func ReadAll(r io.Reader) ([]byte, error)
func Copy(dst Writer, src Reader) (written int64, err error)
ReadAll 通過 io.Reader 這個(gè)接口將 io.Reader 的實(shí)現(xiàn)與 ReadAll 所在的包低耦合的水平組合在一起了。類似的水平組合“模式”還有 wrapper、middleware 等,這里就不展開了,在后面講到 interface 時(shí)再詳細(xì)敘述。
此外,Go 語言內(nèi)置的并發(fā)能力也可以通過組合的方式實(shí)現(xiàn)“對(duì)計(jì)算能力的串聯(lián)”,比如:通過 goroutine+channel 的組合實(shí)現(xiàn)類似 Unix Pipe 的能力。
綜上,組合原則的應(yīng)用塑造了 Go 程序的骨架結(jié)構(gòu)。類型嵌入為類型提供的垂直擴(kuò)展能力,interface 是水平組合的關(guān)鍵,它好比程序肌體上的“關(guān)節(jié)”,給予連接“關(guān)節(jié)”的兩個(gè)部分各自“自由活動(dòng)”的能力,而整體上又實(shí)現(xiàn)了某種功能。組合也讓遵循“簡(jiǎn)單”原則的 Go 語言的表現(xiàn)力絲毫不遜色于其他復(fù)雜的主流編程語言。