實現優雅的 New 函數創建對象

介紹 如何 優雅的實現 New 函數 並使其保持易於調用和擴展

golang 沒有構造函數,所以通常的做法是爲自定義對象提供一個 New 函數來創建對象,比如有一個自定義的 Cat 類型,就需要創建一個 NewCat 函數來完成 Cat 的內存分配以及一些初始化設定。

type Cat struct {
	name string
}

func NewCat(name string) *Cat {
	return &Cat{
		name: name,
	}
}

上述 NewCat 是一個 New 函數,但不是一個優雅的 New 函數,因爲這種寫法存在明顯的缺陷,如果我們的 Cat 還需要一個 color 屬性指定她的顏色,你不得不創建一個類似 NewCat2(name string,color Color) 的函數,但這還沒完又過了一段時間 Cat 變得更加強大還需要一個 Age 屬性你又要創建一個 NewCat3 函數。顯然我們需要一個優雅的方案來實現 New,這個 New 函數需要能夠方便擴充並且不會對已有代碼造成傷害。本文就是探討如何解決這個問題,文章結尾給出了最終的解決方案,不過我們先來看看其它語言以及一些不太優雅的方案是怎麼解決問題的。

參數類型

這是最原始的 c 語言的做法,但很有效所以其它語言裏面也有類似的代碼,其原理是定義一個 Options 類型包含了所有的屬性,比如我們爲 Cat 定義一個 CatOptions 裏面包含了所有的初始化參數:

type Cat struct {
	name string
}
type CatOptions struct {
	Name string
}

func NewCat(opts *CatOptions) *Cat {
	return &Cat{
		name: opts.Name,
	}
}

現在你不需要創建 NewCat2 NewCat3 … 等一系列函數了,用戶永遠只需要調用 NewCat 便能夠得到一個新的 Cat ,並且當擴充 Cat 時 你只需要修改 CatOptions 的定義 和 NewCat 的實現即可,但這對調用者來說是優雅的因爲一切細節都被你隱藏起來了,但對實現者來說並不算優雅因爲你需要不斷的修改 Cat 實現爲新的屬性設置上合適的默認值,以便調用者可以使用舊代碼調用你的新的 Cat

type Cat struct {
	name  string
	color uint8
}
type CatOptions struct {
	Name  string
	Color uint8
}

func NewCat(opts *CatOptions) *Cat {
	color := opts.Color
	if color == 0 {
		color = 1
	}
	return &Cat{
		name:  opts.Name,
		color: color,
	}
}

此外這種實現方案最大的問題還在於當 Options 包含太多字段時可能會嚇跑調用者。

函數重載

對於一些支持重載函數的語言,提供的解決方案就是重載構造函數比如 c++

class Cat{
    Cat();
    Cat(string name);
    Cat(string name,int color);
};

重載看上去很美但卻引發了新的問題,關鍵在於位置參數上,比如有個 age 參數你無法定義 Cat(string name,int age) 因爲這個簽名和 帶color 的參數一樣 所以你只能像下面這樣重載:

class Cat{
    Cat();
    Cat(string name);
    Cat(string name,int color);
    Cat(string name,int color,int age);
};

這時如果新的調用者只想傳入 age 而使用默認的 color 是辦不到的,你通常只能再提供了一個默認 color 導出值給調用者傳入構造函數。

命名參數

新興的語言提供了命名參數來解決這一問題,目前看來應該是最優雅的方案了,但 golang 語言並不支持,不過我們還是來看下她們長什麼樣子,下面是 dart 的示例代碼:

class Cat {
  final String name;
  final int color;
  final int age;
  Cat(
    String name, { // 花括號定義命名參數
    int color = 1,
    int age = 2,
  })  : name = name,
        color = color,
        age = age;
  speak() {
    print(
        "i'm a cat, my name is ${name}, my favorite color is $color, my age is $age.");
  }
}

main() {
  Cat(
    "kate",
    age: 10, // 設置需要調整的 命名參數
  ).speak();
}

命名參數是本喵知曉的目前爲止最優雅的方案,並且命名參數不止可用在優雅的創建對象上,一般函數也可使用,唯一可惜的是 golang 語言並不支持,然也無需太過惋惜畢竟 golang 的優勢是簡單,並且就優雅的創建對象來說 golang 也有合適的方案。

Option 接口

這種方案最早是本喵在看 grpc 的代碼時學到的,鑑於 grpc 由 google 維護,所以這種方案也算是官方認可的方案了,原理是將上述 c 方案和 golang 的接口配合起來使用。

  1. 定義一個私有的 Options 類型
  2. 定義一個公開的 Option 接口 接口函數接受 Options 指針
  3. 提供各種 WithXXX 函數實現 Option 接口來調整 Options 參數
  4. 使用 New(…opts Option) 來創建對象

如此當需要擴展構造屬性時 只需要修改 Options 類型 並增加一個 WithXXX 函數即可

package main

import "fmt"

// 定義默認參數值
var defaultOptions = catOptions{
	name:  `none`,
	color: 1,
	age:   5,
}

// 定義 私有的 參數 類型
type catOptions struct {
	name  string
	color int
	age   int
}

// CatOption 初始化接口
type CatOption interface {
	apply(*catOptions)
}

// 實現 CatOption 接口 幫助創建 With 函數
type funcCatOption struct {
	f func(*catOptions)
}

func (fdo *funcCatOption) apply(do *catOptions) {
	fdo.f(do)
}
func newCatFuncOption(f func(*catOptions)) *funcCatOption {
	return &funcCatOption{
		f: f,
	}
}

func WithCatName(name string) CatOption {
	return newCatFuncOption(func(o *catOptions) {
		o.name = name
	})
}
func WithCatColor(color int) CatOption {
	return newCatFuncOption(func(o *catOptions) {
		o.color = color
	})
}
func WithCatAge(age int) CatOption {
	return newCatFuncOption(func(o *catOptions) {
		o.age = age
	})
}

type Cat struct {
	opts *catOptions // 保存 構造參數
}

func NewCat(opt ...CatOption) *Cat {
	opts := defaultOptions
	for _, o := range opt {
		o.apply(&opts)
	}
	return &Cat{
		opts: &opts,
	}
}
func (c *Cat) Speak() {
	fmt.Printf("i'm a cat, my name is %s, my favorite color is %d, my age is %d.\n",
		c.opts.name, c.opts.color, c.opts.age,
	)
}
func main() {
	cat := NewCat(
		WithCatName(`kate`),
		WithCatAge(2),
	)
	cat.Speak()
}

雖然和命名參數比起來麻煩了一點,但在沒有命名參數時對於需要擴展或初始化參數複雜時應該算是最優雅的方案之一了,此外這種寫法不限與 golang 任何支持接口的語言都可以使用這種方式來讓創建對象的實現和調用變得優雅。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *