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,来满足我们如此个性化(奇葩)的需求了。