Golang

策略模式

策略模式的简单定义

一个服务定义一个抽象的接口,而接口可以有多种实现方式,在使用过程中,服务可以对不同的实现做替换。

操作系统中,打开一个.go 文件,可能有很多方式。vscode,sublime,vim等,我们也可以设置默认的打开方式。抽象来看,这些也可以看作是各种策略,可以指定策略来完成我们自定义的操作。 而前提是系统给我们提供了通用的接口,让我们来实现这些策略。

一个简单栗子

mysql
1
2
3
4
5
6
7
8
9

import  _ "github.com/go-sql-driver/mysql"
import  "database/sql"

func doSomething(){
    if db, err := sql.Open("mysql", dsn); err == nil {
        // do something
    }
}
database/sql
database/sqldatabase/sql/driver
 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

// 注册驱动
func Register(name string, driver driver.Driver) {
	driversMu.Lock()
	defer driversMu.Unlock()
	if driver == nil {
		panic("sql: Register driver is nil")
	}
	if _, dup := drivers[name]; dup {
		panic("sql: Register called twice for driver " + name)
	}
	drivers[name] = driver
}

// 删除所有注册的驱动
func unregisterAllDrivers() {
	driversMu.Lock()
	defer driversMu.Unlock()
	// For tests.
	drivers = make(map[string]driver.Driver)
}

// 当前注册了哪些驱动
// Drivers returns a sorted list of the names of the registered drivers.
func Drivers() []string {
	driversMu.RLock()
	defer driversMu.RUnlock()
	var list []string
	for name := range drivers {
		list = append(list, name)
	}
	sort.Strings(list)
	return list
}
github.com/go-sql-driver/mysql@v1.4.1
1
2
3
func init() {
    sql.Register("mysql", &MySQLDriver{})
}
*MySQLDriver

当然,抽象考虑的话,这就是一个策略模式的实现。提供了注册策略的接口,当数据源切换时,可以任意切换相应的驱动(策略)。

如果代码看的不尽兴,我们可以再看一个易懂的例子。

另一个简单的例子

Kafka 是一个非常经典的消息队列,Kafka消费者可以按照消费组的方式进行消费,当多个客户端按照同一个消费组消费消费同一个主题(Topic)的消息时,需要按照一定的策略将客户端与Partition的对应关系协调好,这样多个客户端才能正常消费,这就是Consumer Group 的Reblance。

github.com/shopify/sarama

其中,分配策略接口定义如下:

1
2
3
4
5
6
7
8
type BalanceStrategy interface {
	// Name uniquely identifies the strategy.
	Name() string

	// Plan accepts a map of `memberID -> metadata` and a map of `topic -> partitions`
	// and returns a distribution plan.
	Plan(members map[string]ConsumerGroupMemberMetadata, topics map[string][]int32) (BalanceStrategyPlan, error)
}
balanceStrategy
BalanceStrategyRange
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func(plan BalanceStrategyPlan, memberIDs []string, topic string, partitions []int32) {
	step := float64(len(partitions)) / float64(len(memberIDs))

	for i, memberID := range memberIDs {
		pos := float64(i)
		min := int(math.Floor(pos*step + 0.5))
		max := int(math.Floor((pos+1)*step + 0.5))
		plan.Add(memberID, topic, partitions[min:max]...)
	}
}
BalanceStrategyRoundRobin
1
2
3
4
5
6
func(plan BalanceStrategyPlan, memberIDs []string, topic string, partitions []int32) {
	for i, part := range partitions {
		memberID := memberIDs[i%len(memberIDs)]
		plan.Add(memberID, topic, part)
	}
}

不同的策略,可以实现客户端与partition的不同对应关系。 如果我们碰到这样一个棘手的问题: 需要消费同一个topic,同一个消费组,需要多个服务在不同机器上同时启动,但是机器层次不齐。当流量大时,有些机器负载比较大可能会挂机,那我们可能实现一个reblance策略,将配置高的机器多分配partition,配置低的机器少分配些partition,来满足我们如此个性化(奇葩)的需求了。