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