let 连续复制_如何避免因复制某行代码并连续多次粘贴而导致的bug?

代码可以粗略分为,通用代码和业务代码。这种划分方式并非唯一,不同角度有不同的划分方式。

通用代码和业务代码,实际没有明确的界限。只是普遍来说,通用代码目的明确,可以复用于很多地方,比较稳定,因外部而引起的变动较少。而业务代码,经常因外部原因而变化,比如产品需求、公司活动、节日庆典等等。

大多数程序员日常工作,其实是在写业务代码。自然也可以从业务代码中,逐渐抽出一些通用代码,但业务逻辑还是避免不了的。对于通用代码,自然是求稳,测试性能,编写单元测试。但对于业务代码,全部编写单元测试是不现实的,因为业务变化太快,写的单元测试很容易就过时,单元测试代码也是代码,也需要维护。

对于业务代码,应该思考如何让其容易修改,而非如何不被修改。

复制粘贴代码之所以糟糕,并非是复制粘贴那个时刻,而是将修改问题推到了日后。假如只考虑目前,要实现一个类似的新功能,复制粘贴,改几个名字或数字,很快就得到成果;往往比你先去重构整理代码,抽取函数再调用,要快得多。只是日后需要修改时,复制出来的代码都要分别修改,复制 10 次,就需要修改 10 个地方。漏了 1 个地方,就会出现 Bug 了。

闲话休提,言归正传。除了封装成函数,另外一个减少重复的有效手段是表格驱动。表格驱动,是将原有代码拆分成,表格和操作表格的代码,这里的表格是指数组或者字典。单看这句话大概不会明白,下面举例子。

比如我有代码创建一个按钮。

let button0 = UIButton()

button0.setTitle("Red Button", for: .normal)

button0.setTitleColor(UIColor.red, for: .normal)

self.view.addSubview(button0)

之后我再想加一个按钮,这时可以不做任何重构,直接复制粘贴,改一下名字,这样重复了 4 行代码,还不算严重(假如重复的代码太多,比如超过 10 行,就不能直接复制)。变成

let button0 = UIButton()

button0.setTitle("Red Button", for: .normal)

button0.setTitleColor(UIColor.red, for: .normal)

self.view.addSubview(button0)

let button1 = UIButton()

button1.setTitle("Blue Button", for: .normal)

button1.setTitleColor(UIColor.blue, for: .normal)

self.view.addSubview(button1)

之后我又想加一个按钮,这时就不能复制粘贴了。代码只出现一次,是孤例;出现两次,可能是巧合;但事不过三,出现三次就需要重构了。观察到重复的代码有 title 和 color 不一样,于是表格只有两项,原有的代码重构成。

let infos = [

("Red Button", UIColor.red),

("Blue Button", UIColor.blue),

];

infos.forEach { info in

let button = UIButton()

button.setTitle(info.0, for: .normal)

button.setTitleColor(info.1, for: .normal)

self.view.addSubview(button)

}

重构时,并不实现任何新功能,也不改变代码行为。重构后,我想加个黄色的按钮,只需要在表格中添加一项。

let infos = [

("Red Button", UIColor.red),

("Blue Button", UIColor.blue),

("Yellow Button", UIColor.yellow),

];

xxxx

同样思路,我们来解决题主的例子,用同一个函数从磁盘载入三个不同的文件。假设这是模型文件,已经有函数 loadModelFromFile,最开始只有一行。

let model0 = loadModelFromFile("example/star.model")

载入另一个模型,复制粘贴一行,变成

let model0 = loadModelFromFile("example/star.model")

let model1 = loadModelFromFile("example/moon.model")

假如需要载入其它模型,就需要重构了。将路径抽取成表格,就演变成:

let paths = [

"example/star.model",

"example/moon.model",

"example/sun.model",

];

let models = paths.map { path in

let model = loadModelFromFile(path)

return model

}

以后再修改,只需要在表格中插入一行。假如担心路径复制后忘记改名字怎么办呢?特别是表格逐渐变长的时候。这时可以稍微加个检查。

extension Array where Element: Hashable {

func toSet() -> Set {

var set = Set()

for v in self {

set.insert(v)

}

return set

}

func hasDuplicate() -> Bool {

return self.toSet().count != self.count

}

}

let paths = [

"example/star.model",

"example/moon.model",

"example/moon.model"

]

assert(!paths.hasDuplicate())

xxxx

我们添加了一个 assert 作为额外检查,这个 assert 在 release 版本是会被优化掉的。而在开发 debug 阶段,假如我忘记改名字了,paths 就有重复元素(如上面代码有两行 "example/moon.model")就会触发了 assert,于是就知道写错了。重构和添加 assert 检查之后,我可以确信我这段代码是不会出错的。

另外注意到,我们已经从业务代码中,为数组抽出 toSet 和 hasDuplicate 两个通用函数。这两个通用函数脱离具体业务,可以复用于其它地方。当这两个通用函数第一次使用时,可以直接写在当前业务文件中。当其它地方也需要用到这些函数时,我们可以将其转移到通用代码库中,顺手为其编写单元测试代码。单元测试代码主要测试你不确定,可能出错的地方,但其实这两个函数很简单,一眼看过去就知道不会错的,甚至连单元测试也可以免了。之后假如觉得 hasDuplicate 这个名字不够好,就取个更好的名字,将用到的地方顺手再改改。慢慢地,就会形成一些通用库,来帮助更好地写业务代码。

业务代码通常没有什么性能问题,怎么清晰怎么写。将其转移到通用代码,就需要多考虑性能了。比如上述的 hasDuplicate,保存接口不变,我们将其优化一下。碰到重复元素就可以立即退出了,没有必要扫描整个数组。

extension Array where Element: Hashable {

func hasDuplicate() -> Bool {

var set = Set(minimumCapacity: self.count)

for v in self {

let (inserted, _) = set.insert(v)

if !inserted {

return true

}

}

return false

}

}

表格驱动在《代码大全》这本书有详细描述。上述例子使用 Swift 语言,而这种思路独立于语言。实际上是将程序分解成,数据和操作数据的代码,表格驱动的表格就是数据。这样操作数据的代码保持不变,修改的只是数据。而修改数据会比修改代码更容易。抽成的表格,假如将来有需要,也很容易从主程序中分离出来,独立做成配置。配置最开始是纯数据,如果配置当中又需要一定的逻辑,就会慢慢演变成了 DSL。

也可以扩展阅读这篇文章《程序的本质复杂性和元语言抽象》,这篇文章中将程序分解成,逻辑和控制,讲述方式和术语可能不一样,而道理是相通的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值