從Go語言實現模板設計模式淺談Go的抽象能力

首先拋出一個觀點,那就是Go的抽象能力的確不如Java這種嚴格的OOP語言強。具體表現之一就是模板模式的實現。

模板的實現

模板模式是OOP編程中的一把神兵利器,用好了能夠提高代碼的複用程度,大大提高開發效率。例如,我們可以在父類中定義完成一個任務的幾個步驟並分別給出一個默認實現,然後子類繼承父類,子類只需要重寫自己感興趣的方法即可,剩餘邏輯都可以複用父類的代碼。Spring源碼中就大量充斥着這樣的套路。但是在go語言中,連類都沒有,更別提繼承了,那如何才能使出這種套路呢?答案就是內嵌匿名結構體。

如果一個struct A中內嵌了另一個匿名的struct B, 那麼A就可以【直接】訪問B中所有的字段和方法。這句話翻譯成代碼就是這個樣子的:

type B struct {
	bField string
}

func (b *B) bMethod()  {
}

type A struct {
	B // A可以【直接】訪問B裏所有字段和方法
}

a := new(A)
a.bField  // OK
a.bMethod() // OK

這就是Go語言間接實現繼承的唯一方法,內嵌匿名結構體。如果在定義A時給B進行了命名,比如b, 那調用時就只能a.b.bField(), a.b.bMethod()了,完全失去了繼承的意義。

實現模板模式最核心的問題在於,如何將事先定義好的步驟【延遲】到子類中執行。在Java中,這是通過編譯器和JVM共同保證的,也就是說不需要程序員關心此事。但是在go中不存在類似於abstract, extends這樣的關鍵字,那就需要我們通過編寫代碼來保證父類定義的方法在子類中執行這一條件。具體思路爲,在"父"結構體中定義多個能夠共同完成任務的函數類型字段,"子"結構體在內嵌"父"結構體時,將"父"結構體的這些函數類型字段賦值爲自己的方法實現即可。例如,我們有模板結構體TaskTemplate, 它有beforeTask func()afterTask()兩個函數字段:

// 任務模板類, 定義一個執行的執行步驟
type TaskTemplate struct {
	// "子類"給此字段賦值
	beforeTask func()
	// "子類"給此字段賦值
	afterTask func()
}

然後再定義一個將所有函數組合起來的runTask()方法:

func (task *TaskTemplate) inTask() {
	fmt.Println("in task")
}

// 調用所有任務步驟
func (task *TaskTemplate) runTask() {
	task.beforeTask()
	task.inTask()
	task.afterTask()
}

最後我們再來定義實際執行任務的MyTaskTemplate結構體:

// 具體執行任務的構造體
type MyTaskTemplate struct {
	// "繼承"模板類
	TaskTemplate
}

// 實現模板中的beforeTask方法
func (my *MyTaskTemplate) beforeTask() {
	fmt.Println("my before task")
}
// 實現模板中的afterTask方法
func (my *MyTaskTemplate) afterTask() {
	fmt.Println("my after task")
}
// 構造一個MyTaskTemplate結構體
func NewMyTaskTemplate() *MyTaskTemplate {
	myTask := new(MyTaskTemplate)

	// 將"父類"中的函數字段設爲自己的實現
	myTask.TaskTemplate.beforeTask = myTask.beforeTask
	myTask.TaskTemplate.afterTask = myTask.afterTask

	return myTask
}

這裏重點就在於充當構造函數的NewMyTaskTemplate()函數,在這裏面我們完成了將"父類"中的"方法"替換成自己實現的任務。現在就可以執行一下了:

func TestExtends(t *testing.T) {
	task := NewMyTaskTemplate()
	task.runTask()
}

輸出:

my before task
in task
my after task

可以看到,我們創建的是充當子類的MyTaskTemplate結構體變量,但in task這行輸出是在"父類"中完成的,其他兩行輸出則是纔是由"子類"完成的。那麼這還有一個問題,直接創建子結構體其實達不到模板的意義,我們希望能使用一個通用的類型來引用MyTaskTemplate,然後只調用此能用類型的runTask()方法即可。這裏顯然不能使用TaskTemplate類型的變量來引用MyTaskTemplate,因爲他們並不是同一個類型,go語言裏是沒有繼承的概念的,我們只是做了一個簡單的結構體嵌套而已。要想達到這一目的,就必須再定義一個包含了runTask()方法的接口:

type RunTask interface {
	runTask()
}

然後就可以使用此接口類型來引用任何一個實現了runTask()方法的結構體了:

func TestExtends(t *testing.T) {
	task := NewMyTaskTemplate()
	invokeRunTask(task)
}

func invokeRunTask(task RunTask) {
	task.runTask()
}

最後輸出是完全一樣的。

Go的抽象能力不夠強

至此我們使用Go語言【勉強】實現了模板模式。但這是十分不優雅的,問題很多:

  • 編譯器無法強制"子類"來實現"父類"定義的步驟方法, 編寫"子類"有可能會忘記實現,但這一錯誤要到運行時才能被發現。
  • 需要在"子類"中手動替換父類函數變量的值。如果忘了或者根本沒有使用NewXXXX()方法而是直接&TaskTemplate{}的話,錯誤也是要運行時才能發現的。

上面的兩個問題完全無解,無優雅解。

Go語言的設計哲學是簡單和簡潔,即使用最少的關鍵字、最少的語法來實現最常用的功能。這一點在協程上體現的淋漓盡致,比如無需關心協程調度,無需考慮是否block線程, 同步的代碼實際爲異步執行和極簡的go關鍵字等。但這樣也是有代價的,那就是犧牲了抽象能力:

砍掉了繼承,導致不容易優雅實現各種設計模式;
砍掉了重載,導致無法使用同一函數名來精簡需要不同參數的情況;
砍掉了泛型,導致要麼interface{}漫天飛要麼重複編碼;

當然,這些問題也不會讓go走不了路,最多也就是走的姿勢不夠優雅而已。很多崇尚極簡的人會有一種卸下語法糖包袱返璞歸真的感覺。個人希望官方能在go 2.0的大版本升級中做一下改善,在保證語言簡單、簡潔的前提下稍微支持一點OOP裏好用的特性,那樣就真是接近完美了!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章