背景
在开发中经常会发生一个变量在多个地方赋值的情况。最讨厌的情况是写代码改了某个变量的值,一运行发现还是没改,因为又在后面一个不知道哪里的地方被改了。怎么办呢?
首先我认为一个变量在多个地方赋值是一个非常常见且自然的现象,我们要做的不是谴责、减少这种行为,而是要好好管理。
所以诉求就变成,希望新改的逻辑一定能生效以及希望知道一个变量最终被哪个逻辑赋值。
我想到可以用优先级的形式去管理赋值,每次对同一个变量赋值都要声明一个优先级,变量最终的值是优先级最高的赋值。于是我写下了下面的Go代码,思路就是用一个变量存下当前赋值的最高优先级以及对应的值。
Battle实现
type Priority interface {
~int | ~int32 | ~int64
}
// Battle: 用于同一个变量在多个地方赋值,battle一个优先级决定最终的赋值
type Battle[P Priority, V any] struct {
TargetValue *V
CurrentValue *V
CurrentPriority P
}
// NewBattle: 初始化要设置一个最大的priority
func NewBattle[P Priority, V any](priority P, targetValue *V) *Battle[P, V] {
return &Battle[P, V]{
TargetValue: targetValue,
CurrentPriority: priority,
}
}
// Set: value传nil表示放弃赋值
func (b *Battle[P, V]) Set(priority P, value *V) {
if value == nil {
return
}
if priority > b.CurrentPriority {
return
}
b.CurrentPriority = priority
b.CurrentValue = value
}
// SetVal: 帮忙转换为指针,这个不会放弃赋值
func (b *Battle[P, V]) SetVal(priority P, value V) {
b.Set(priority, &value)
}
// SetTarget: 为不影响其他人直接修改原变量,仅在最后对原变量赋值一次,同时打log
func (b *Battle[P, V]) SetTarget(logName string) {
if b.CurrentValue != nil {
*b.TargetValue = *b.CurrentValue
}
if logName != "" {
log.Printf("Battle %s %d", logName, b.CurrentPriority)
}
}
// GetBattlePtrValue: 用于本来类型*V就是指针,并想用nil来表示不赋值的语义的时候,获得**V,用于Set函数
func GetBattlePtrValue[V any](value *V) **V {
if value == nil {
return nil
}
return &value
}
Demo1及讲解
下面是一个Demo。
type BattleDemoValue int
const (
BattleDemoValueLogic2 BattleDemoValue = iota
BattleDemoValueLogic1 BattleDemoValue = iota
BattleDemoValueOldLogic BattleDemoValue = iota
)
func Demo1(hitLogic2 bool) {
var demoValue string
battleDemoValue := NewBattle(BattleDemoValueOldLogic, &demoValue)
var logic2Value *string
if hitLogic2 {
logic2 := "logic2"
logic2Value = &logic2
}
battleDemoValue.Set(BattleDemoValueLogic2, logic2Value)
battleDemoValue.SetVal(BattleDemoValueLogic1, "logic1")
battleDemoValue.SetTarget("DemoValue")
UseDemoValue(demoValue)
}
下面讲解Demo1。
Demo2
如果目标变量是一个指针,我们还想使用Set的话,就需要两层指针,用起来很别扭。于是我加了一个辅助函数GetBattlePtrValue来应对这种情况。Demo2演示其使用。
func Demo2(hitLogic2 bool) {
var demoValue *string
battleDemoValue := NewBattle(BattleDemoValueOldLogic, &demoValue)
var logic2Value *string
if hitLogic2 {
logic2 := "logic2"
logic2Value = &logic2
}
battleDemoValue.Set(BattleDemoValueLogic2, GetBattlePtrValue(logic2Value))
logic1 := "logic1"
battleDemoValue.SetVal(BattleDemoValueLogic1, &logic1)
battleDemoValue.SetTarget("DemoValue")
UseDemoValue(*demoValue)
}
讨论
下面是讨论。
- 首先是我们的诉求:希望新改的逻辑一定能生效以及希望知道一个变量最终被哪个逻辑赋值。这两者都满足了,前提是SetTarget到使用目标变量之间不能再出现对目标变量的赋值。新逻辑一定生效是用把新逻辑优先级设为最高的方法。判断变量最终被哪个逻辑赋值是靠SetTarget打的日志。至于这个前提,我认为,只要有合作者有完全的修改代码的权限,就没有办法从理论上阻止合作者乱改,人家甚至可以删库跑路,但是这一般不会发生,因为我们都很自觉。我认为这不是一个问题,就算真的改坏了,也很好发现,很好改。
- 除了满足了我们的诉求,用Battle还有以下好处:
- 可以看到一个全局的需求列表,以及各需求的优先级关系,比较坦诚清晰,而且优先级可以轻松调整。
- 对老代码修改很小,可以直接在老代码上用Battle开发新需求。比如在Demo1中,可能存在很多老代码直接修改目标变量。我们把最低的优先级BattleDemoValueOldLogic分给这些修改。
- 不要求所有赋值都使用Battle。其实跟上一点是一样的。其他合作者可以继续用自己喜欢的方式开发。
- 对不同需求的赋值顺序没有依赖,最终都是以优先级决定赋值。赋值也可以散落在不同函数中,使用起来比较灵活。
- 关于用Battle的成本。对于一个新接入Battle的变量,需要进行Demo1讲解的1~4步,对于一个已接入Battle的变量新加入一个需求,需要进行1、3步。对于后者,成本基本等同于不使用Battle。但对于前者,还是比不使用Battle多了一点代码量,尤其是需要接入多个变量的情况。我认为多个目标变量可共用一个优先级类型,这样可以降低一点代码量。不过这里的度要自行把握,如果所有变量共用一个优先级类型,则所有需求会叠在一起,难以看清哪些需求会改哪个目标变量;如果一个目标变量一个优先级类型,则会创建过多类型,过于繁琐。不过总的来说,我认为Battle已经是一个额外代码量比较少的工具了。
- 关于并发。Battle显然不是并发安全的。在设计之初我想过设计成并发安全,但后来发现大部分场景下都不需要并发安全,要改成并发安全成本也不高,所以并发安全是一个过度设计,就砍掉了。