“ 不求甚解亦或是固步自封,都是从事IT行业所不可取的。如果只会写一手好代码,却不会思考,那只能称作码农,而不是Coder。——尼古拉斯·小Q
前言
我做过很多烂项目。烂项目有益个人成长,但早期它挺困扰我。因为它让我怀疑自己就是那个败事鬼:为啥我参与的项目都如此不堪?这种自责随着职位的升高有所缓解。但原因绝不是可以训斥我的人在减少。这有点象透过一个镜头观察世界。
多年来这个镜头逐渐拉远,视野扩大,我得以看到越来越多的东西。伴随这个过程的,是思维的伸展:什么力量在驱动软件开发进程?软件开发困境(high failure rate)是如何形成的?是我们用的不成熟的框架?那位喜欢指责下属的经理?还是今年糟糕的天气?
(high failure rate):IT项目、尤其是软件研发项目的成功率长期维持在惨淡的水平上。读者可以参考这个来自IPLA的报告:(http://calleam.com/WTPF/?page_id=1445)
本文是我对此问题的一点看法。虽没带来解决问题的秘密武器,但也许能在思想上给同行们一点启示。
软件开发最大挑战是?
如果你问周围同事,软件开发的最大挑战来自哪里?得到的答复可能五花八门:技术、管理、需求、甚至睡眠。如果你问我同样的问题,我的今天回答将是“变化”。这里的“变化”尤指“未来的未知变化”,它基本与“不确定性”、“混沌”、“复杂”等单词等价。因此本文会混用这些术语。
我们先看看变化是怎么发生的。
从历史说起
计算机发明之初,它还是一种巨大、昂贵、只有少数科学家能接触到的设备。这种设备死气沉沉、一无用处,除非你给它输入一组指令让它“活”起来。这些指令就是我们现在说的“软件”。虽然比现在的软件简单很多,但当时世上没几个人会写它。要修改它,同样要劳烦那些尊贵的科学家。事实上,除了专业的小圈子,没人会对它提出修改要求。可以猜测那时的软件开发没啥挑战,因为硬件的问题远比软件的多。
随后几十年的技术进步推动着计算机硬件快速演进:标准化、小型化、价格降低并进入家庭。最后软件开发成为专门的行业从硬件中独立出来。软件所面对的用户数量迅速扩大。一旦意识到软件是发挥计算机硬件能力的唯一方式(用CPU煎鸡蛋似乎只有少数人会用到),人们对它的欲求便开始迅速膨胀。而且他们发现,相比房子、洗衣机、甚至一张桌子,软件这种商品的修改要容易的多,还不用啥材料!比如,“把保存按钮给我换成红色能花你多长时间?!”。软件开发人员的苦难历程就是这么开始的。
今天,手机应用、互联网服务等软件形式能把企业的价值以最快的速度传递到最大规模的用户群。这迅速拉近了企业与用户的距离。事实上,距离如此之近,以至企业可以腾挪的空间大大缩小了。同时竞争让用户的选择变的更多。在这么短的距离上,用户的选择可以迅速决定企业的生死。
而软件的创造者,即开发人员的日子似乎正变得愈发艰难。他们不得不努力迎合用户多变的需求,如果他们知道需求的话。对那些没有确切掌握需求而又急于占领市场的开发团队,情况会更糟。他们唯一的机会可能来自“敏捷性”——适应变化的能力:尽早发布产品、密集收集用户反馈、尽快分析并调整产品、再发布。通过快速迭代,他们在维持市场存在的同时,不断提升自己产品的竞争力。
内部的复杂性
其实情况还不算太糟,因为我还没谈到软件创造者自己内部的问题。
当今软件开发的复杂性已经显著增大。比如仅从代码行数看,几十万行代码构成的软件产品已经很常见,上千万行的产品也并不稀罕。在这么大的复杂度面前,压根就没有“银弹”,“一招鲜”是绝对不够的。专业分工、团队组织、工具优化、甚至心理咨询,我们要把所有能用到的武器投入战场方能应付。
可努力之后我们会发现,除了外部世界带给我们的苦,我们自身已经成了痛苦的另一个来源。因为分工、协作、工具等等诸多变量已经让研发过程变成一个复杂的动力系统。很多时候这一系统本身的问题会成为我们的主要挑战。在有些项目中,那些曾被我们痛恨的趾高气扬的客户,现在被折磨成与我们一起苦苦挣扎、惺惺相惜的同命人,就是这个原因。
两线作战
从上文很容易总结出软件开发工作的进展过程。它通常从未明确的需求开始(这样说并不严格,因为任何项目在开始之初总或多或少掌握了一些需求。但未知或可能变化的需求总是我们最头痛的。本文想阐明的是这部分内容。)
这些未知需求构成了一个黑暗的混沌世界。开发团队靠大无畏的业务经理找到突破口,然后向外摸索前进。慢慢地,他们澄清了一些不确定性,并据此建立起一些秩序(设计、代码实现等等)。随着团队开疆辟土,混沌初开、曙光浮现。但内部的混沌也随之升起。它是未明确的职责、被误解的需求、未掌握的技巧、待研究的算法等等内容的混合物。
内部的复杂性随着工作规模的扩大不断增长。直到有一天团队发现自己陷入两线作战的局面,一面是外部持续的压力,一面是内部增长的混乱。他们竭尽所能、苦苦支撑,希望在任何一条战线崩溃前把产品交付客户了事。有些团队深陷其中,甚至会忘记 “交付”这个最初的目标。
变化的影响
变化催生复杂结构
构成软件的基本材料是程序代码。变化对开发进程的影响最终会体现到代码上。所以我想通过下面的 Python 程序来展示[^why_pydemo]变化的影响。这些代码很简单,希望不懂Python语言的读者也能明白。
假定我们要写一个软件,姑且叫它 hello。用户说只要这个软件能打印出 “hello world” 就行了。那么我们能很快完成这个软件 1.0 版:
print “hello world”
毫无疑问,你必须事先知道你要打印“hello world”字符串才能写下上述代码。可需求会变化。比如用户还想打印出“Hi people”。好在我们不会把用户的话奉为圭臬。我们能预见到这种变化。所以我们会象下面这样实现它,姑且称其为 hello-2.0 :
def print_message(msg):
print msg
通过定义一个函数`print_message`,我们现在可以应付更多的可能性了。定义并实现这个函数的两行代码,就是我们(暂时)冻结的内容。我们靠远见成功地在不确定性中找到了可以固定的东西。
但远见覆盖不住全部的可能性。比如,变化可能再次发生。这次用户可能说,消息不仅要打印出来,还要能输出到文件。当然,这个变化依然难不倒我们。我们会用新的方式实现 3.0 版的 hello,即象下面这样把它分成两个模块`foo`和`bar`。
foo.py(v3.0):
def put_message(send, msg):
send(msg)
bar.py(v3.0):
def send_to_screen(msg):
print msg
def send_to_file(msg):
out = open(out.txt, w)
out.write(msg)
out.close()
很多机智的开发人员会在项目之初,即第一次听到用户说“只要这个软件能打印出hello world就好啦”后就设计出 hello-3.0 的结构来。这样虽然覆盖不住全部的可能性,但至少,我们固定住了一部分东西,即模块`foo`,而把变化交给`bar`模块来应付。
这就是我们软件开发者的生存方式。由于程序员不可能对未确定的要求编写代码,团队的各种努力都是在帮助程序员在一定范围内消灭不确定性,即固化某些东西。我们一点点澄清和冻结不确定因素,直到范围扩大到可以交付的标准。在此过程中, _变化_ 迫使系统的复杂性增大并最终形成一种特定的结构。
虽然上例展示的需求变化是来自组织外部,但同样的道理也适用于内部。这里我就不再举例详述了。另外,我是用编程来展示变化的影响,但我相信它所包含的方法论含义也可以应用在其他方面,包括体系架构、团队组织等。
比如对常见的多层软件架构,如果未来不会出现变化,我们是不需要分层的:没有系统压力的变化,我们就不需为伸缩性(scalability)而把承担压力的主要业务逻辑部分单独出来做一层;没有用户交互的变化,我们没必要把界面处理从业务逻辑中抽出来作为独立的GUI层;没有数据存储系统的变化,我们把数据访问层抽出来也是多余的… 读者可以按这个角度联想一下,就很容易意识到,是变化这一力量,促使软件系统、甚至创作软件的社会化组织——研发团队,在微观和宏观层面产生出特定的结构。这与生物学中的适应性进化多么相似啊。
变化带来破坏
已经固化的代码(结构)并不是万无一失的。它会受到变化的侵袭。这种入侵会沿着代码的依赖路径向整个代码库蔓延,在复杂性上叠加混乱。我们还是以上面的 hello-3.0 为例。在 3.0 版中,为了分离出可以固化的东西即`foo`模块,我们把易变的代码放到了另一个模块`bar`中。这创建了一个依赖关系,即`foo`对`bar`的依赖,即“依赖路径”。现在,假定一个未预料到的变化来临,比如,如果用户希望文件名可以用其他的怎么办?好吧,你说,我们接受这个变化,因为我们本来也没打算冻结`bar`模块(把它分离出来就是为了应对这一情况的)。然后你把它改成:
bar.py(v4.0):
def send_to_screen(msg, dest):
print msg
def send_to_file(msg, dest):
out = open(dest, w)
out.write(msg)
out.close()
现在,`bar`模块中的各个输出函数都增加了一个参数`dest`以支持可变的输出文件名。但现在你发现,这一变化已经不可阻挡地蔓延到`foo`模块了。因为你不得不修改它来适应这个新的参数:
foo.py(v4.0):
def put_message(send, msg, dest):
send(msg, dest)
当然你可以用一些语言特色,比如变长参数,来保持`foo`模块的稳定。比如你可以这样来实现 3.0 版的`foo`:
def put_message(send, *args):
send(*args)
有很多技巧(比如设计模式)帮我们处理类似情况。但这些技巧无法应对全部的可能性。变化对结构的破坏,或多或少,是无法避免的。
上例展示的变化蔓延在开发期就可以被发现。但还有很多情况要在运行期才可能暴露出来。那些不确定性将对我们固定的结构产生更难预料的后果(与构建期相比,严重的问题好像总是在交付给客户后才出现,而它们的原因经常又是诡异难寻)。
我们可以改进系统设计和团队组织方式来缓解困境。但是,不仅我们无法杜绝变化,我们也无法完全阻止变化的蔓延。这就象病毒混在血液里向身体其他部分扩散,我们不能掐断自己的动脉来阻绝它。
与其对抗,不如拥抱
传统工程领域,比如机械、建筑等,在资源组织、设计、施工等一系例工作中也会对未来的不确定性做准备。比如桥梁设计中要考虑负荷范围,机械零件设计中要考虑公差等。但变化对这些领域的影响,似乎远没有对软件工程这么深远。它对我们的影响体现在方方面面,比如:
我们用对象、函数、包等程序结构把变化的影响限制在一定范围内;
我们设计出类型系统(type_sys)来跟踪数据结构的变化;
我们用编译器(Compiler)在开发期帮我们找出各种不一致问题;
我们用测试来检查系统运行时各种可能性产生的影响;
我们不再迷恋“大设计”,因为在大量的变化前,它是一种浪费;
我们从步步为营的[瀑布模型][waterfall]转向敏捷模型,以期尽早获得反馈消除不确定性;
我们用精英化的小团队而非人海战术,从而避免信息在由人构成的传播链路中失真(变化);
我们招聘时宁缺毋滥,因为我们相信技艺高超的人能克服人类对未知领域的本能恐惧;
… …
这个列表可以很长。从中你大概能感受到我们对变化的抗争。有一些人采用鸵鸟策略,固执地对变化视而不见或认为它们不会发生。这是否是造成软件困境的原因呢?
我觉得也许我们要改变一下看问题的视角了。我想说的是,响应变化不应再是我们临时的、不得以的工作。**它就是我们的软件开发工作本身**。这不是在否定那些缺乏未知因素的开发工作。那一类开发项目依然存在,只是它们代表不了未来,可能也引不起推动行业进步的那些精英们的兴趣。
这一视角也许能帮我们找到走出困境的新方法。事实上,现代软件工程中的许多概念,比如敏捷开发、DevOps,我认为是与本文的视角一致的。所以如果以这样的视角来理解上述概念,不仅能帮助我们正确地应用它们,还可能发展出更符合自己实际情况的独特做法。再不济,它也能缓解我们疲于应付时的精神痛苦。
后记
如果不存在变化,软件开发就简单多了。但不论在外部还是内部,变化都永远存在。进入信息时代后,变化正在以更大的规模、更快的速度发生着。完全应对这未知的洪流是不可能的。我们应坦然面对,并找到一个可以接受当面冲击的小根据地,然后逐渐扩大它。这个思路可以应用在日常的工作决策中,比如选择合适的测试时机。
老司机介绍王锦全,男,本科学历,1996年毕业于中南大学机电工程学院。长期从事软的研发和管理工作,先后担任过软件工程师、系统架构师、CTO等职务。曾服务于多家公司,包括世界100强、国内大中型国有、私营软件企业等,也有过自主创业的经历。目前是杭州一家中型软件企业的技术总监,主要负责公司的管理制度优化、敏捷研发推广以及大数据技术研发等工作。他是资深的 Java 和 Python 程序员,也是 Clojure(Lisp) 爱好者。读书和思考是他的爱好。 您在其 linkedin主页( http://cn.linkedin.com/in/johnwangwjq )可了解他的更详细经历。
▽
延展阅读(点击标题):
前Facebook早期员工:我是如何肉身翻墙,又回国创业的?| 大咖说回放
【ArchSummit深圳2016】15大精彩专题,50位大咖讲师,Cloudera、Hearsay Social、Uber、LinkedIn、Twitter等等,你将为哪家公司技术点赞?阿里巴巴、腾讯、百度、美团、饿了么、滴滴、新浪微博等等,核心业务技术较量谁又能触动你的神经…最精彩的技术切磋从这开始,ArchSummit九折门票倒计时,详情请戳阅读原文
本文系InfoQ原创首发,未经授权谢绝转载。