本文试图从直观的角度阐述如何做好常规的软件工程,保持良好的开发进度和可维护性,同时让项目经验对技术社区具有启发意义。Github 源代码托管和社交网站里的 JavaScript 和 Ruby 等开源代码繁多火爆的盛景无疑充分地说明了这一点。
试着把十根左右的火柴散乱地倒在地上,你会发觉你无法一下子看清有多少根。请注意,这里是看清,接着你可以在几秒之内慢慢地数清有多少根火柴。
这就是一般人类思维的极限和特点,人无法同时在脑子直观地在同一个层面掌握六七个以上不同事物。
再换个例子就是手机号码比生日难记的多。手机号码首三位是给运营商用的,后面的 8 位被用于地区和用户编码了,对于人脑记忆而言毫无规则。而生日首先已经被人类经验分成了年份和月日这样两层,年份除去世纪就是一个两位数了,而月日是由大小不超过 12 的两个简单数字组成的。
试想你现在要去写一个猜单词游戏客户端,目标是如何在有限步骤内猜中更多的单词。这个需求可不是你可以用一个 MVC 框架可以套用的,你得自己去组织代码。
一开始你大概设计了基本的算法框架,先在一个文件里写着,不断的抽取方法,刚开始看着还不错。
当代码越来越多失去控制的时候,你考虑是否可以把算法独立出去作为一个库存在了。这样你就有了主程序和算法库两个文件。
接着你发现对方的服务器有时发生超时或内部错误,网络访问速度对于一次要执行几百次的程序也太慢,因此需要搭建本地的 Mock 服务器,这样你又多了 remote 和 local 两套实现机制,加上公用部分就是三个文件了。
如果再算上 README 等文档,单词数据源文件,测试代码,你就会发现现在这差不多是一个成规模的项目了,它有十来个按目录分组的文件。
以上的例子并非虚构,它实际来源于我参加 Strikingly 的限时两天远程面试编程项目 Hangman ,以上代码迭代重构过程完全可以从 commit 信息里得知。
经历过 #混沌时代# 的我们,要去写一个涉及到数据库操作,业务逻辑,页面渲染,缓存管理,等等的复杂应用,如果没有一个像 Rails 这样便捷的 Web 框架,而自己一个一个去实现,那是一件多大工作量的事情。
有了框架,一切都很美满,用 Rails 漂亮的 DSL 配置下代码就可以了。
我相信没有哪个 Rails 用户不会为 RESTful 架构简洁的风格所体现出来的哲学所折服,GET, POST, PUT, DELETE 四种 HTTP 动词,index, new, create, show, edit, update, destroy 七种 Controller 方法,基本解决了大部分需求。别人来二次维护项目时也能很快地上手。
然而实际的世界远远没有这么简单,Rails 框架并不能覆盖到你全部的业务。有些人可能看过一则传授怎样画马的短篇漫画,
最后一步背后的工作恰恰是最关键的,和耗时最长的,也即是我们常说的 一万小时定律 ,充分地强调了对技艺的理解力,经验,思考,和风格等。
当然在开源如此繁华和被倡导的年代,你可以使用插件,比如 exception_notification 去管理你的程序异常。
再有更大型的,比如 devise 用户认证,kaminari 分页,等等,它们在 Rails 的 MVC 三层都扎根了,甚至还包括了数据库和前端 javascript。
慢慢地,有 Rails 开发经验的人,慢慢会发现总有一些不太适合放在 Rails MVC 三层里面的公共代码,于是一般都会把它们放到 config/initializers/或 lib/目录下。
但是总有一些比较大的东西让你 Hold 不住,举个例子,一个 Model 的业务逻辑是从压缩包里导入多层次的内容,并且得处理格式不规范等各种异常,那样很快就让这个 Model 增加了两百多行,方法数量超过了二十个,即使放到单独的文件里也还嫌多。描述过程的方法和公用的方法混合在一起相互引用,变量共享,中间结果缓存,很快就变得难以维护和扩展。
其实这个过程和 Ruby 社区的 HTTP 中间件处理库 Rack 做的事情在形式上有点类似,该库把 HTTP 请求的 Header 和 body 封装成 Rack 对象,然后被一个一个封装成模块的业务需求顺序地处理掉,过程类似于剥洋葱,最后把结果返回给客户端。
所以我们可以把这塞在一个文件的方法重构为以下概念上独立的三个部分。
这样就清晰多了,调试也很方便。另外你也可以把它独立出去作为一个模块去处理了,这样 model 就瘦了不少。
让我们再引申一下,对比之前全都放在一个 Model 类里去操作的做法,新的其实是建立了自己的结构,也就是框架。
这里面最重要的一点就是,一旦你写的代码和 Rails 本身的 MVC 框架无关时,代码组织超出了一定规模,而且它没有对应的开源库可以帮助,那么是有必要去专门为这个问题构造一个模块,框架,或者 DSL 了。
著名科学哲学家 Thomas S. Kuhn 写过一本《科学革命的结构》,里面提出了"范式转移"这一概念。举个物理学的范式转移例子,关于物质运动的解释,已经历经了从亚里士多德时代的模糊度量,到牛顿定律的理想化计算,再到爱因斯坦相对论的更精细化表述,这样三种体系。精度的变化只是表象中的其中一个度量,最关键的是里面大部分概念已经发生了本质性的变化,或者说那些名字已经不是原来的所指。比如物体的质量在在牛顿力学里不可变的,而在爱因斯坦相对论里因为速度的改变,质量也会发生变化。
同样,对于构建复杂应用的软件工程师来说,我们所使用的程序语言和软件框架就某个已启动项目来说一般很少发生变化,因为它们通常是业界认可和采用的模式和工具。编程过程中发生的各种技术问题,包括命名不清晰不一致,重复造轮子,破坏单一职责原则,Copy-Paste Style,意大利面条式代码,没有恰当的注释文档,等等,这里不一一列举,它们已经在 Martin Folwer 的著作《重构》一书中被详细论述。
针对一般的网站开发,我以为 MVC 只相当于三段论这种原则性的方法论而已,而做好一个复杂系统的根本前提是在于你的计算机科学方面的系统知识,数年实际项目编程经验,以及风格化的思考(这通常就是一种品味)。当我们面对的项目越复杂时,不断精细化和抽象化的思考和重构应该贯穿在项目的各个生命周期。于是,Rails, Mongoid, Devise 等从中而出。
很多人都赞同编程是一种创造性活动,再甚之是一种艺术,大可以和绘画等艺术形式媲美。
我以为这是对的,很多人都认同旧项目一般都难以维护,特别是糟糕的代码。同样对于一幅画来说,如果乱糟糟一堆,色彩,元素关系,细节刻画都很差劲,稍微修修补补绝对没有画龙点睛的效果,这后来者真的还不如重画。写程序对比其他艺术形式的一个好处就是可以通过采用或抽取开源组件来获得更好的可维护性。
Paul Graham 在《黑客与画家》一书中写到,"黑客与画家都是在试图创作出优秀的作品。他们本质上都不是在做研究,虽然在创作过程中,他们可能会发现一些新技术。"
而实际上我以为就艺术这一层面上两者并没有多大关系,唯一的共同点就是都倾向于视觉审美。代码不能拿来听,也不能拿来思考(算法还可以稍微拿来思考一下,但其本质是数学),它只能被拿来看,拿来用计算机去运行,在各个模块或函数中之间调试(那算法来说,这里就包含了具体工程上的很多细节优化实现)。
这里我想举一个有趣的例子,人们一般提起黑客,就想到那些用 Vim, Emacs 等纯文本界面编辑器的生活在黑底绿字终端下的大牛,IDE 太笨重,而且无助于他们对代码的编写,调试,和思考。我不想比较其中的优劣,或者谁更正统,我认为纯文本目录导航浏览更接近把代码在放在大脑里思考的视觉化模式。
大家看看 Vim 的操作指南,它的使用模式里居然区分了浏览和编辑等模式,这对用惯了其他电脑程序的人而言无异于初次见到数学的等于符号和编程里的等于符号不等价一样让人惊异。在 Vim 里,我们用的更多的是浏览模式,编辑模式只有输入纯内容而已;而在浏览模式,你可以让光标在字与词,段落之间快速移动,可以把上下行对换,可以批量对齐,甚至可以拷贝某个长方形区块的文本。在此我想说的是,黑客和画家一样,思考的是元素与元素的关系,局部和整体的关系,从远观的良好命名的代码目录结构里可以看出项目架构(这个一般被 Rails 这种框架负责了),也可以从细观的在单个文件的某个类或者函数中把握其局部的独立性(这个就是代码良好的耦合性和单一职责原则)。
而如果采用 IDE 编写,它会依据行业公认的软件工程经验去组织和自动化代码编写和管理,强壮的 Java 社区的整个工业体系无疑说明了这一点。但是它严重破坏了代码组织的直观,架构和编程完全可以分开,编程从业人员变成其中一颗螺丝钉,编程也失去了创造性的乐趣,导致很多人错误地变成了只会一种软件框架的和吃青春饭的"码农",很多人觉得三十岁后必然得转向管理或业务。
同是视觉性的绘画创作(我先声明我不会画画,所以言论可能有失偏颇),它不像诗歌,小说,音乐一样是生活在时间里的艺术,它力求的是直观,从全局和细部都可以欣赏,现代绘画有些已经抛弃细部了,它的画只能在一定距离外才能被有度量尺度限制的人眼看懂和欣赏它的美,比如印象派画家 莫奈的 。
拿编程和绘画打个比方就是,好的应用程序应该以 user story 为基本单位去勾勒程序架构,像一幅大型古典油画一样每个细部都可以拿出来欣赏把玩,而不是被扁平地代入一个一个现成的充满棱角的技术框架。其中具体的算法只是采用的材质不同,采用的开源库可能只是某个人物的帽子或者职业特征,技术架构体现了构图。真正优美的软件工程,应该让作为故事的皮肤和血肉恰到好处地覆盖骨架,使人一眼就能明白那是一个美人,而不是畸形得像是失败实验品的科学怪物。
事实上任何高级的创作必然是纯手工的,或者手工在里面起了必不可少的作用,这个看看奢侈品或咨询行业就知道了。
试想一下,规划或者维护项目的时候,一般相关负责人都会画出一个类似思维导图的项目架构图,这个被业界普遍认可和采用。但是有些没亲手架构并实现过大型复杂多系统的人很容易会拿业务架构去套技术架构,搞出一个又一个貌合神离的子系统,而实际做的时候,变成了一个个只能靠 HTTP 通讯的孤立系统,类似于日常见到靠 OAuth 相互认证的互不关联的多个互联网网站一样,当超出一个 MVC 框架可以 hold 住时,他们开始束手无策。
针对人类思维特点,类似于键盘人体工程学,我们在编写软件时应该注意代码在浏览上对于人类思维的直观,除了比较熟知的一个函数里不要写太多代码外,同样一个作为基本单元的功能也最好不要超出六七个相对不同的函数集。有些人可能不太明白,他们确确实实见到了很多包括有名的开源项目里都有单文件里几百行甚至几千行的代码,但是这些优秀的代码通常都是在一个层次里面把函数集合放在单独抽取的模块里。不要担心做不到这一点,用不同颜色去标记不同国家地区的地图都可以仅用 四种颜色来染色 。
机械的本质就是齿轮之间的咬合,而相互驱动的齿轮绝对不能像混乱的意大利面条一样到处都是死结。
前段时间同事和我分享了一篇 Tell, Don't Ask 的文章,讲述了模块调用之间松耦合的原则,所谓"不在其位,不谋其政",此观点也可以被归纳为 Law of Demeter 。
Law of Demeter 处理的是已经划分好模块后如何相互之间通讯和单一职责的事情,而人类同时只能处理有限数量的相似对象的原则指明了什么时候应该适当重构的这条界限。