于小微处见大功夫

2019-09-17 17:23:17 浏览数 (2)

今天 code review 时,见到这样一段代码:

代码语言:javascript复制
def update_balance(input_value) do
  ...
  commission_rate = get_config("comission_rate")
  balance1 = data1.balance   input_value * (1 - commission_rate)
  ...
  balance2 = data2.balance   input_value * commission_rate
  ...
end

为了方便阐述,我把变量名稍微做了调整,隐去不必要的细节。这段代码乍一看问题不大,传入的 input_value 被分成两部分,一部分是这次交易的佣金,计入了 balance2,一部分是剩下的部分,计入了 balance1。

仔细想想,这段代码有两个大的问题:

  1. 不符合 Open-close Principle (OCP)
  2. 不安全

我们一个个看。

Open-close Principle

Open-close Principle 是说 代码对扩展开放,对修改封闭 (software entities should be open for extension, but closed for modification)。这虽然是来自 OOP 的一个原则,但我们也可以将其推广到 OOP 之外的地方。

在上面的代码里,计算交易的佣金这件事和 update_balance 是彼此独立的,update_balance 不必知道也不该知道如何计算交易的佣金的细节就能完成它自己的任务。之所以说不该知道,是因为 update_balance 知道了这个细节,那么万一交易佣金的计算方式发生改变,那么必然涉及 update_balance 的改变,而因为这个改变散落在这个函数的不同地方,容易牵一发而动全身。

那么可能会发生什么改变呢?这就有赖于程序员的产品感观。commission_rate 是个佣金的比率,既然交易的佣金可以按比例抽成,那么是不是将来有可能按固定值抽成?或者交易额小于一个数量,固定抽成,大于一个数量,按比例抽成,甚至再复杂些,阶梯累进抽成?

所以我们应该讲这个计算的过程独立出来,为其创建一个函数,比如:calc_commission,那么 update_balance 就变为:

代码语言:javascript复制
def update_balance(input_value) do
  ...
  commission = calc_commission(input_value)
  balance1 = data1.balance   input_value - commission
  ...
  balance2 = data2.balance   commission
  ...
end

def calc_commission(input_value) do
  commission_rate = get_config("commission_rate")
  round(input_value * commission_rate)
end

这样,不管日后计算 commission 的方式如何变化,只要接口保持不变,update_balance 都自然适配,这便是:对扩展开放,对修改封闭。

代码安全

写代码是一个严肃而严谨的过程,然而,受限于程序员主观的意识和客观条件(撰写过程中是否能够不被打断分心,项目是否火烧眉毛,老板天天催等),很多时候,代码很容易写得随意而不安全。上面的代码从安全的角度看上去没有毛病,仔细一想,commission_rate 错误的取值可以让结果朝着相反的意图发展 —— 比如大于 1 的 commission_rate。也许你会说,我们可以在 commission_rate 进入系统边境时设置哨所,进行检查,不允许不合规范的数据进入系统。大部分时候,这个策略是没有问题的,但在一些重要的场合,层层累进的安全策略是必要的。比如,黑客也许精心构造了可以导致缓冲区溢出的数据,将边境处的哨所撂倒,使得检查失效;更有可能的是,之后这段代码被复用在另一个系统,而那个系统并没有提供相关的边境检查,从而让黑客有机可乘。

大部分如上的问题都是函数的定义域和值域不明确导致。程序员如果没有训练自己这方面的意识,容易犯错;当然编程语言也没有提供很好的工具来支持明确的函数定义域和值域的设置 —— 即便是强如 haskell 也无法很容易规定类型在某个特定范围取值。在 OCP 那一节里,我们抽取除了 calc_commission,对于目前这个版本的函数,不管 commission_rate 怎么变,计算出来的 commission 都应该在 [0, input_value] 之间,所以 calc_commission 应该为:

对应的代码为:

代码语言:javascript复制
def calc_commission(input_value) do
  commission_rate = get_config("commission_rate")
  max(0, min(input_value, round(input_value * commission_rate)))
end

这里通过 min/max 将这个函数的值域锁死在 [0, input_value] 间,如果 commission_rate 取值不合法,commission 最大是 input_value, 最小是 0,这样杜绝了溢出的风险。

在这里,我们可以养成的一个良好的习惯是,为所写的关键函数撰写数学公式。因为公式本身是严谨的,在表达公式的时候不会遗漏定义域中的不合理份子,从而让函数的值域有一个严谨的范围。之后,对着公式写代码,代码便很难有逻辑不严谨之处。

贤者时刻

曾子寝疾,病,乐正子春坐于床下。曾元、曾申坐于足,童子隅坐而执烛。童子曰:“华而睆。大夫之箦与?”子春曰:“止!”曾子闻之。瞿然曰:“呼!”曰:“华而睆,大夫之箦与?”曾子曰:“然。斯季孙只赐也。我未之能易也。元,起,易箦。”自此始也。曾子曰:“夫子之病革矣,不可以变,幸而至于旦,请敬易之。”曾子曰:“尔之爱我也不如彼,君子之爱人也以德,细人之爱人也以姑息。吾何求哉?吾得正而毙焉,斯已矣。”举扶而易之,反席未安而没。

曾子病倒在床上,病情严重。乐正子春坐在床下,曾元、曾申坐在脚旁,童仆坐在墙角,手拿烛火。童仆说:“席子花纹华丽光洁,是大夫用的席子吧?”乐正子春说:“住口!”曾子听到了,突然惊醒过来说:“啊!”童仆又说到:“席子花纹华丽光洁,是大夫用的席子吧?”曾子说:“是的,这是季孙送给我的,我没有力气换掉它。元啊,扶我起来,把席子换掉。”曾元说:“您老人家的病已很危急了,不能移动,希望能等到天亮,再让我来换掉。”曾子说:“你爱我不如爱那童仆,君子爱人是用德行,小人爱人是姑息迁就。我现在还要求什么呢?我只盼望死得合于正礼罢了。”于是大家扶起曾子,换了席子,再把他扶回到床上,还没有放安稳,曾子就去世了。

这是著名的「曾子易箦」的故事,按照周礼,曾子没有做过大夫,是不能使用大夫专用的席子。虽然实属无意,假如他死在大夫专用的席子上,那就是“非礼”了,因而即便处于弥留之际,他也依然命令儿子给他更换席子。这个故事抠字眼的话我们可以认为曾子迂腐,不可效法,但故事本身传达的是「勿以恶小而为之」,「千里之堤毁于蚁穴」,姑息迁就是「细人之爱」,会损毁德行。两三行细小的代码,如果不注重推敲,也会让整个系统变得脆弱。

0 人点赞