小问题也能收获有益启示。
例子一
请看下面的代码(实际工程代码简化而来)。从字符串中解析出反弹 IP。
这段代码原意是,对reverseFuncNames 里的 New-Object 元素做特殊处理,然后对除 New-Object 之外的其它元素做其它相同处理。这段代码有什么问题?
var (reverseFuncNames = []string{"[Net.Sockets.TcpClient]::New", "Connect", "New-Object"}noPort = "-1"
)// ParseReverseShellInfo 解析反弹IP
func ParseReverseShellInfo(str string) []string {if string_utils.IsNotEmpty(str) {var otherConditions boolvar otherConditions2 boolif str == "New-Object" && otherConditions {// do something and return}reverseFuncNamesRest := reverseFuncNames[:len(reverseFuncNames)-1]if slice_utils.SliceContains(reverseFuncNamesRest, str) && otherConditions2 {// do something and return}}return make([]string, 0)
}
不错,这段代码确实实现了它的原意。那么,问题在哪里呢?细心的读者应该看出来了:假设 reverseFuncNames 在 New-Object 之后 新增了元素,那么最后一个元素将不能被处理。
问题不会在当前出现,而是在新增元素之后出现。从这个小例子可以得到什么启发?
- 当代码处理与数组中的位置关联起来,且数组可能会在未来变化时,代码处理可能会出问题。这是写代码时欠缺可扩展性考量导致的。合理的写法是什么?把 "New-Object" 放在第一个位置,然后修改为 reverseFuncNamesRest := reverseFuncNames[1:]。 这样,即使后面新增元素,也能正常处理。
- 这确实只是编程的一个小例子。不过,它告诉我们的是,即使很微小的问题,也应当考虑可扩展,使得未来新增元素无需做任何改动。不同的实现方式有着不同的效果。不适当的实现方式会隐藏导致未来出错的坑。
例子二
我喜欢代码重构。代码重构能够让代码更加整洁,也能是提升代码技艺的一种有效手段,同时也不失为一种消磨时间的方式(我没有摸鱼哦!)。即使是小小的挪动代码,也能带来“爽感”。
移动代码,将一段逻辑从流程中抽离放在一个函数里,然后在流程中调用这个函数。通常是一种不会出错的重构方式。不过,最近我踩了一个坑。简而言之是这样的:
func fillBase() {// do something// set score
}func fillbaseInfo() {fillBase()// set severity
}
然后我做了个什么事情呢?把 set score 的代码抽离到 c() 方法,然后变成了这样:
func fillBase() {// do something
}func fillbaseInfo() {fillBase()// set severitysetScore()
}func setScore() {// set score
}
这有什么问题 ? 我改变了 set severity 和 set score 的顺序,而 severity 的值是依赖 score 的!当然,从简化的代码来看,似乎非常明显,不过,加上中间一大段代码隔着,然后方法名又如此相似,且当时我并没有意识到 severity 和 score 是有关联的,因此移动代码出了错,踩了坑。
这个小例子能带给我们什么启示?
- 即使是最基本的移动代码,也不是全然安全的。如果移动的代码所操作的变量和流程中的代码的变量有依赖关系,那么一定要保证移动代码之后,顺序依赖关系也不变。
- 关联关系一定要注意处理。就像你改动了长方形的宽,就要同时更新长方形的周长和面积。道理虽然浅显,但深入到错综复杂的业务之后,不明显的关联关系很容易导致问题,尤其是你所处理的模块和其它不熟悉的模块产生关联关系的时候。一方面,要有全局视野,避免只看到局部;另一方面,要有意识去处理这种关联关系。而处理的前提是记录。由此看来,软件开发的协作还是比较复杂的。