Go語言是徹底的面向組合的併發語言

banq發表於2014-09-15

面向組合程式設計從AOP的Mixin,然後到Ruby的Traits,直至DCI設計,包括Scala的trait的組合設計,這些都有一個共同特點,組合特性是顯式的,也就是說要用專門語法來宣告組合。其實組合設計應該是物件導向設計中很自然的一種方式,也就是說,只要你使用面嚮物件語言,隱式上你就具備了強大的組合能力,而無需另外用trait這些語法專門實現。

來自Less is Exponentially More認為:

If C++ and Java are about type hierarchies and the taxonomy of types, Go is about composition.

如果說C++和Java是關於型別的層次和分類,那麼Go是關於組合。

組合

我們看看Go語言申明一個具有屬性欄位和方法的型別竟然採取完全平等的方式定義,然後將它們組合成一起,假設一個物件Door有一個屬性標識當前狀態,兩個方法開啟和關閉,GO程式碼如下:

type Door struct {
    opened bool
}
 
func (d *Door) Open() {
    d.opened = true
}
 
func (d *Door) Close() {
    d.opened = false
}
<p>

這三大行程式碼分別是定義一個struct(靜態結構)和兩個動態功能方法Open()和Close()。Go中是沒有Java的Class類的概念,因此,我們不能將靜態結構和動態行為混合在一個類中,其實一般情況下,將兩者分離是有好處的,因為結構一般是不變的,具有不可變性,而需要混合的情況基本是需要狀態可變的,這時使用Go的CSP模型(類似Actor模型)即可。Go語言的這種動靜分離設計非常巧妙。

Go語言倡導使用組合替代繼承,那麼組合能否實現多型性呢?繼承中多型性是通過多個子類繼承父類來實現,組合是如何實現?下面看看Go語言的組合多型性的實現:

package main

import (
	"log"
)

type B struct{  }
func (b B) foo() {     log.Printf("...") }

type A struct {
    B
}

func main() {
	var a A
	a.foo()
}
<p>

以上程式碼點按http://play.golang.org/p/IU7xq62WC-直接執行。

foo()是B的方法 但是B嵌入了A中,那麼其方法可以看成A的方法被直接呼叫。這雖然很像Java中A extends B,也就是A繼承了B。Go語言通過組合實現了Java傳統語言中使用繼承實現的多型性。

如果上述被嵌入A中的B是一個介面怎麼辦呢?在Java中如果一個欄位是介面,我們需要通過依賴注入或IOC容器將介面的實現子類注入進去,這是我們使用組合方式經常碰到的場景,當然我們一直期待Java能夠將依賴注入加入語言機制,從Java9的提前披露設計中我們絲毫看不到這種傾向,不知道那些參與Java規範設計的人是否經常使用Java開發企業應用,基於JVM的Scala的依賴注入也是如此。

假設B是一個介面:

package main

import (
	"log"
)

type B interface {
    foo() 
}

type A struct {
    B
}

func (a A) foo() {     log.Printf("...") }

func main() {
	var a A
	a.foo()
}
<p>

http://play.golang.org/p/p3wATT9I2F

我們為A實現介面B的foo()方法,是不是類似將func (a A) foo()注入到了type A struct中,當然因為這裡B是介面,不能使用a.B.foo()呼叫,而前面一個例子B是一個實體,可以使用a.B.foo()

因為Go語言自然的語言組合能力,我們不必藉助額外依賴注入框架實現組合+注入了,這大概是我初期最為驚訝的。同時,那些所謂Mixin或trait功能都自然地融合在這種元件實現中了,比如A本來沒有方法foo(),Go語言本身將B的foo()編織weaving進入了A。

併發

回到文章開始舉例Door,Door是一個有狀態的物件,但是守護改變其狀態的行為卻被拆解在外面,通過組合對接進去,我個人認為這其實是一種貧血模型或者失血模型,類似Java的只有setter/getter方法的POJO,而我們在DDD設計中,倡導使用富模型聚合根來實現,通過聚合根守衛狀態,而且聚合根之間通過訊息事件驅動,Go的CSP模型可以幫助我們實現。

Go 的CSP模型是使用channel 和goroutine 開發並行程式的能力,協程是一種綠色執行緒,不是真正的作業系統的執行緒,而是使用作業系統的一個執行緒進行不斷切換使用,類似Node.JS的單執行緒非同步併發原理,這種花銷很小的併發能力比多執行緒併發要更經濟。你不能在JVM上實現Actor, 綠色執行緒和CSP

CSP如下程式碼:

package main

import "fmt"
import "time"

func sleepAndTalk(secs time.Duration, msg string, c chan string) {
    time.Sleep(secs * time.Second)
    c <- msg
}

func main() {
    c := make(chan string)

    go sleepAndTalk(0, "Hello", c)
    go sleepAndTalk(1, "Gophers!", c)
    go sleepAndTalk(2, "What's", c)
    go sleepAndTalk(3, "up?", c)

    for i := 0; i < 4; i++ {
        fmt.Printf("%v ", <-c)
    }
}
<p>

http://play.golang.org/p/vIH6o-o-HB

c := make(chan string)是生成一個通道,go標識協程,同時執行。協程與channel的關係類似於Java中執行緒與佇列。多個CSP作為聚合根能夠串聯起來:

Go語言是徹底的面向組合的併發語言

參考:Go語言入門教程

相關文章