真实世界的Go设计模式 - 工厂模式

工厂模式(Factory pattern)是一种创建型模式,就是用来创建新对象的一种设计模式,它可以将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象。

23种设计模式中包含抽象工厂模式,工厂方法模式,其他也有人总结出简单工厂模式。这个工厂大量的依赖接口、抽象类和具体的类实现。在Go中,才不会有这么复杂的工厂创建模式,Go中最常见的工厂模式类似简单工厂模式,而且一般都是通过New或者NewXXX来实现。

比如我们要实现一个存储数据结构,它可能是基于内存的存储,也可能是一个基于磁盘的存储,抑或者是一个基于临时文件的存储,不管怎么样,我们先定义一个Store接口:

1
2
3
4
5
6
7
package data
import "io"
type Store interface {
Open(string) (io.ReadWriteCloser, error)
}

再定义不同的Store实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package data
type StorageType int
const (
DiskStorage StorageType = 1 << iota
TempStorage
MemoryStorage
)
func NewStore(t StorageType) Store {
switch t {
case MemoryStorage:
return newMemoryStorage( /*...*/ )
case DiskStorage:
return newDiskStorage( /*...*/ )
default:
return newTempStorage( /*...*/ )
}
}

使用方法如下:

1
2
3
4
5
s, _ := data.NewStore(data.MemoryStorage)
f, _ := s.Open("file")
n, _ := f.Write([]byte("data"))
defer f.Close()

(以上例子摘自https://github.com/tmrts/go-patterns)

更进一步,甚至我们都不会创建一个接口,比如Go标准库的net/http.NewRequestWithContext,用来创建一个*http.Request对象。

根据body类型的不同它会创建不同的request.GetBody,这里没有使用接口,一个struct足够了,因为GetBody是一个函数指针,你可以根据参数的不同生成不同的Request。这里充分利用了Go的type switch、func pointer等特性,不用生成复杂的接口和具体类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
...
u.Host = removeEmptyPort(u.Host)
req := &Request{
ctx: ctx,
Method: method,
URL: u,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(Header),
Body: rc,
Host: u.Host,
}
if body != nil {
switch v := body.(type) {
case *bytes.Buffer:
req.ContentLength = int64(v.Len())
buf := v.Bytes()
req.GetBody = func() (io.ReadCloser, error) {
r := bytes.NewReader(buf)
return io.NopCloser(r), nil
}
case *bytes.Reader:
req.ContentLength = int64(v.Len())
snapshot := *v
req.GetBody = func() (io.ReadCloser, error) {
r := snapshot
return io.NopCloser(&r), nil
}
case *strings.Reader:
req.ContentLength = int64(v.Len())
snapshot := *v
req.GetBody = func() (io.ReadCloser, error) {
r := snapshot
return io.NopCloser(&r), nil
}
default:
}
if req.GetBody != nil && req.ContentLength == 0 {
req.Body = NoBody
req.GetBody = func() (io.ReadCloser, error) { return NoBody, nil }
}
}
return req, nil
}

一个更好的例子就是database/sql下的Open方法

1
func Open(driverName, dataSourceName string) (*DB, error)

你子需要提供不同的数据库类型名,以及dcn,就能生成一个对应的*DB对象,注意DB是struct,并没有定义一个 DB类型的接口。

它的具体实现是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func Open(driverName, dataSourceName string) (*DB, error) {
driversMu.RLock()
driveri, ok := drivers[driverName]
driversMu.RUnlock()
if !ok {
return nil, fmt.Errorf("sql: unknown driver %q (forgotten import?)", driverName)
}
if driverCtx, ok := driveri.(driver.DriverContext); ok {
connector, err := driverCtx.OpenConnector(dataSourceName)
if err != nil {
return nil, err
}
return OpenDB(connector), nil
}
return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil
}

它会从一个表(drivers)找到对应的工厂(driver), 然后调用这个工厂的OpenConnector得到一个连接器(或者直接生成一个dsnConnector),最后调用OpenDB创建DB对象。

不同的数据库类型可以通过Register(name string, driver driver.Driver)注册特定的数据库驱动,比如mysql的驱动:

1
2
3
func init() {
sql.Register("mysql", &MySQLDriver{})
}

clickhouse驱动:

1
2
3
4
func init() {
var debugf = func(format string, v ...any) {}
sql.Register("clickhouse", &stdDriver{debugf: debugf})
}

针对这种有具体的不同实现的场景,Go的套路经常是用一个来注册不同的实现,创建的时候查找找到相应的实现方式。如果rpcx微服务框架在支持不同的连接协议时,也是通过查找找到相应的创建方法创建连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func init() {
makeListeners["tcp"] = tcpMakeListener("tcp")
makeListeners["tcp4"] = tcpMakeListener("tcp4")
makeListeners["tcp6"] = tcpMakeListener("tcp6")
makeListeners["http"] = tcpMakeListener("tcp")
makeListeners["ws"] = tcpMakeListener("tcp")
makeListeners["wss"] = tcpMakeListener("tcp")
}
func (s *Server) makeListener(network, address string) (ln net.Listener, err error) {
ml := makeListeners[network]
if ml == nil {
return nil, fmt.Errorf("can not make listener for %s", network)
}
if network == "wss" && s.tlsConfig == nil {
return nil, errors.New("must set tlsconfig for wss")
}
return ml(s, address)
}