《5 天学会画画》


我本来不喜欢那种速成教材,号称 30 天学会英语,7 天学会编程等等。但是却被一本书吸引到了:《5 天学会画画》。几年前一口气看完,今天又看了一遍作者录的视频,也是一口气看完。

当然,由于是一口气看完,所以只花了几个小时,远没有花到 5 天的时间,显然没有最终学会画画,但是给我的启发却是让我终身受用,并且远不止应用在画画上面。

这本书一共介绍了五个技巧,其中给我印象最为深刻的技巧是“画阴形”,或者叫做画“虚空间”。“阴形”相对于“阳形”,拿浮雕举例,凹下去的是阴形(多半是背景),凸起的就是阳形(多半是主体)。

作者拿画一把椅子举例,当初学者准备画一张椅子,并希望画得像(也就是现实主义画派,或者说是素描),想要开始,却无从下手。因为当你开始观察椅子,就会发现细节太多,甚至每一根椅子的腿,由于摆放位置等因素,导致看上去都不一样。
image.png
画阴形的技巧就是,忘掉椅子,去观察椅子背后的虚空间,以及由于椅子的部分遮挡导致的原本完整的虚空间被割断形成的各个多边形形状,这些就是阴形。去画这些阴形要简单得多,无非是一些四边形、三角形或者一些多边形等等,当把阴形画完之后,阳形的椅子自然就在那里了。
image.png
这个绝妙的技巧,利用了一个事实:虚空间和实体共享同样的边线。当你描绘完阴形的边线,阳形就自己显现了。所以几乎在所有的练习中,作者总是会先强调把画面先涂黑,然后你也会看到,作者使用橡皮擦的次数和使用笔尖的次数相当,使用橡皮擦擦出的就是虚空间,而实体在擦的过程中就自己显现了。即使在画人物肖像时,作者仍然是这样做的。
image.png

测试驱动开发


几年前看完这本书,就深受触动,尽管当时我没有立即在画画上面实践这个技巧,但是沉迷于写代码的我,正好开始接触到一种编程技巧:测试驱动开发。可以说这就是编程上的“画阴形”方法,从此在编程上一直使用这个方法至今,爱不释手。

就如同绘画的初学者出于本能会去画阳形,但是由于把控不了实体的复杂性,导致各种变形,以至于最终成果惨不忍睹。初级的编程者会出于本能去尝试直接实现需求,最终发现代码写着写着就在复杂性中失控了,严重的会运行不起来,而好不容易调通了后扔给测试人员去测,祈祷能够顺利过关。但现实是各种被打回,改了这个 Bug 又带来新的 Bug,改好新的 Bug 老的功能点又坏了,就像铺床一样,铺好了这个角,那个角又皱了起来,如此往复陷入一个焦油坑,最终放弃。

测试驱动开发则和本能相反,写实现代码前,先写测试。不妨把实现代码比做阳形,测试代码就是阴形。在面对复杂的需求说明时,先不要去想如何实现,而是去想怎么测试,怎么验收?先写出一个测试用例,就是找到一个虚空间中的基本形。然后实现并且仅仅只实现这个测试用例,使其通过后,再增加下一个能够让测试失败的用例,随后写实现代码,能够同时通过所有的测试用例,如此重复,直到所有的测试用例能够涵盖整个需求说明。当所有的测试用例都通过时,需求就做完了,一个可以工作的软件自然浮现出来了。

这和“画阴形”的绘画技巧异曲同工,这些测试用例,就是需求说明的边界线。

不可能的任务


2013 年我在英孚教育青少儿实验室工作,有一次团队要做的一个需求,是让系统帮助电话销售主管自动分派销售线索,这个自动分派需要做到动态适应,以及公平。一个电话销售团队,由销售主管和销售专员组成,但是销售专员的数量是动态变化的,因为有老人离职和新人加入。并且每个人的熟练程度不一样,同一个人的熟练程度也会随时间变化。我们设想的是让主管只需要在系统中录入专员的姓名和熟练程度,当有变化时只需要添加新人、删除离职的专员,修改熟练程度。这样主管就能从每天繁重的线索分派中解脱出来,花更多时间发展自己以专员的销售技能。

这个需求并没有分配给我,但是到迭代周期的尾声时,我从开发经理处得知我们下周的发布先不上这个功能,原因是测出了很多问题,我当时的感觉就是开发已经进入了一个焦油坑的状态。我当时也是年轻,想要多表现,就暗暗分析起这个需求,在第二天站会上看到 PO 显然还是希望我们能上这个功能,我就提出自己来试试做这个功能,如果能通过测试,还是有希望赶上下一个发布的。

其实当天晚上我已经把需求分析清楚了,得到团队的认同后就直接开始做了。结果是花了一天的时间完成了第一个版本,第二天根据代码评审结果做了些小改。后来测试没问题直接赶上了版本发布。后来我在内部 Wiki 上详细写了一篇文档详细说明了这个需求以及分析过程(后面再撰文分享这个具体算法),并将对其的实现命名为“销售线索的动态强制分布分派算法”,没想到还得到了英孚教育成人英语实验室的架构师的点赞,和一番详细的评论。
image.png

第一个版本上了之后,我本来还准备有机会再优化一下,因为按照算法,是需要将一个累计误差做持久化存储的,以做到历史累计意义上的公平,但是优先级不高,后来竟然一直搁置,再也没有优化过,因为一个次优方案就已经达到了各个利益相关者的预期。我自己回顾下来,从分析需求到上线,只花了 3 天时间,还是挺激动的,觉得自己完成了一个不可能的任务,并且通过内部文档分享间接结识到另一个业务单元的架构师,后来还有幸一起合作过一个项目。后来拿了一个奖状,这个不可能任务应该有所贡献吧。
image.png

这个“销售线索的动态强制分布分派算法”,准备后面再单独分享。我后来回想起这个点子,其实也和“阴形”思想分不开。要支持销售专员的动态设置,还要每次的线索分配都公平,以及把每次的分配汇总成每天、每月、甚至每年,都得是最公平的,顺着这条线思考,就是本能的冲动,而且无从下手。当时我看到尝试实现的代码里充满了各种莫名的 Random(),感觉作者是希望通过祈祷的方式通过测试验收。

其实我是睡觉时在床上思考该怎么解决这个问题的,当时感觉动态和公平是阳性目标,而且细节多,复杂度高,就放弃了这个方向,转而思考它在这个问题空间中切割出的阴形形状会是什么样子,后来就在迷糊中抛弃掉现实,进入虚拟空间,在虚拟空间中,没有整数约束,于是自然有一个绝对符合主管设定的强制分布方案,然后这个公平的阳性目标,就转变成了最小化现实方案与理想方案的误差的阴形目标了。第二天在实现这个“销售线索的动态强制分布分派算法”时,再一次使用了测试驱动开发,虽然具体工具使用上还很生疏,不过在开发经理的帮助下,最终按照正确的姿势完成了,并且只花费了远少于前一个开发所用的时间。

测试驱动开发可以驱动出算法吗?


也许可以,也许根本就用不着。拿上面的例子,“销售线索的动态强制分布分派算法”不是测试驱动出来的,而是受到一个启发得来的,所以这个点子的产生没有用到测试驱动开发,因为还没有开始开发。但是不管这个算法怎么想来的,都推荐使用测试驱动开发去实现它,避免陷入焦油坑啊同志们!

但是如果测试驱动出了算法我也不惊讶,我之前的一个语言解释器,就是从 0 开始测试驱动来的,动手前完全没有设计过:https://zhuanlan.zhihu.com/p/345079513

预测上证指数走势


除了画画和编程,其实生活中也能使用这个阴形思维。假如上证指数每天有 50% 的可能性上涨,那么你有多大的把握保证下周上证指数会上涨呢?

这个问题如果顺着思考,就需要将如下这些概率加起来:

  • 只在星期一上涨的概率
  • 只在星期二上涨的概率
  • ……
  • 在星期一、星期二上涨的概率
  • 在星期以、星期三上涨的概率
  • ……
  • 五天全部上涨的概率


下周指数上涨的概率就是阳性目标,这样分别罗列各种概率最后求和的分而治之思想是很好的,也能得到正确的结果,但是考虑一下它的阴形:即下周不上涨的概率,会显得更加简单,显然是。于是下周上涨的概率就是

当然,这个问题的前提不成立,之所以选择上证指数,是为了醒脑和贴近生活(这年头还有人不看股票的么?)。其对应的严谨但是无趣的例子是:连续抛一枚硬币 5 次,出现正面的可能形是多少?

总结


其实有人会说,这不就是反向思考嘛。对,或者叫换位思考也行。但是我看到《5 天学会画画》里的“画阴形”技法后,觉得“阴形”思维最生动,提到这个词,就能看到一个平面,突然平面上一部分凹陷进去,浮雕显现了出来。难怪有雕刻大师说,我没有在雕刻,我只是把多余的部分凿掉了而已。

反证法,数学上屡见不鲜,在集合论和概率论中,这种思维方式更是屡试不爽。有意思的是,这种我原本以为只是科学思维中的方法,没想到艺术家也是这么思考的。高手都有同样的洞见,这个洞见就是阴阳相对抗,却又互为补充

image.png image.png