软件开发的首要难题是什么?需求的收集?技术的选型?项目管理?都没错,但用一个词来总结那就是:复杂度!《Code Complete》中引用了Brooks(没错,就是写出了著名的《人月神话》的那位)论文《No Silver Bullets: Essence and Accidents of Software Engi- neering》里对软件开发复杂度的阐述,按照亚里士多德的哲学观,将事物的属性分为本质(Essential)和附属(Accidental)。本质属性就是一样东西之所以是它的属性,比如车都要有引擎,但引擎是什么型号的就是附属的属性。软件开发一路发展过来,像编程语言、编译器、开发环境等工具方面都有了长足的进步。可直到今天,我们仍然不能说软件开发很简单,所以就如Brooks所说:软件开发本质上是复杂的,所以首要任务就是管理复杂度!那么哪些元素致使软件开发的复杂度这样高呢?下面就以《Code Complete》第五章为资料,加以一些个人的思考,来看一看软件工程复杂的原因、构成以及如何应对。
正如前文所说,即便我们有了类似自然语言一般的高级编程语言、最自动化的工具等,也无法消灭软件开发的复杂性。因为我们用期望用软件来解决现实世界中的问题,而非最理想情况下的计算(例如算法书里一个简单的数据结构的伪代码),所以我们要处理各种可能的用户输入,潜在的硬件、网络、磁盘等错误,复杂的业务逻辑和UI展示,以及不断随着时间而变化的客户需求。因为现实世界就是如此,所以复杂度难以避免。那现在就来看看真实世界给我们带来了哪些挑战。
软件开发者对自己工作“最引以为傲”的一点可能就是软件开发中横跨的数量级。计算机科学泰斗Dijkstra指出,编程(计算)是唯一横跨9个数量级的智力活动的职业。到今天为止,在一个复杂的大型项目里这个数量级可以很容易就超过15个。任何人,即便是最强大脑,也不可能把一个项目所有的细枝末节都灌入脑海。就像马戏团里的演员,不断地将小球扔入空中再用另一只手接下来,玩的球越多就越容易掉到地上。
不管是面向对象编程(OOD/OOP)还是函数式编程(FP),都不可避免地要管理状态。以面向对象编程为例,状态是很让人苦恼的一点,尤其是状态可变类的对象(Stateful Object),如何能保证其正确性呢?以前还没有这样的感觉,满眼都是面向对象编程的好。但随着工作时间的积累,每次工作需要或者找工作需要时都来一次温故知新。于是就不断地忘记、复习、忘记、复习…… 愈发地不知道怎样才能设计出一个好的对象。各种原则如SOLID、内聚、耦合等等都了解,但心里过不去的坎儿是如何限制那个恶魔——状态。它使原本可以直接进行正确性推导和验证的代码变得“琢磨不透”,一不小心就释放出了“恶魔”,只能祈祷我们有足够的测试用例去覆盖各种情况。关于这个问题网上也有很多呼声,不费力就能Google出来一大堆,甚至有人专门总结了对OOD批评的完整列表《Arguments Against OOP》。我们听听几位大师是怎样说OOD的吧,从中可以窥见一斑。
Alexander Stepanov “compares object orientation unfavourably to generic programming: I find OOP technically unsound. It attempts to decompose the world in terms of interfaces that vary on a single type. To deal with the real problems you need multisorted algebras — families of interfaces that span multiple types. I find OOP philosophically unsound. It claims that everything is an object. Even if it is true it is not very interesting - saying that everything is an object is saying nothing at all.” Paul Graham “has suggested that OOP’s popularity within large companies is due to “large (and frequently changing) groups of mediocre programmers”. According to Graham, the discipline imposed by OOP prevents any one programmer from “doing too much damage”.” 摘取自《Object-oriented_programming Wiki》。Stepanov无法接受OOP将万事万物都归为对象,我们暂且不提这个哲学问题,至少在我们的编程经验里会经常碰到只有数据或只有方法的对象,我们甚至还有专门的设计模式来让这种情况看起来很合理:Strategy、Factory、Command…… 而Graham则直截了当,说OOP是平庸程序员的港湾。
Dijkstra “was interested in formally proven and correct-by-construction programs using the formalism of Hoare triples. But you cannot apply Hoare triples to object-oriented programs hence you cannot prove their correctness. Therefore, for Dijkstra, OOP was crap. To those who heard about “design-by-contract” Bertrand Mayer and Eiffel, I’ve used that, I was part of research on that and I can tell you it doesn’t work. I’m troubled by the fact we don’t have a mathematical model for OOP. We have Turing machines for imperative (procedural) programming, lambda-calculus for functional programming and even pi-calculus (and CSP by C.A.R. Hoare again and other variations) for event-based and distributed programming, but nothing for OOP. So the question of “what is a ‘correct’ OO program?”, cannot even be defined; Indeed OOP was (and still is) adopted mostly for UI and “business applications” where ultimate correctness isn’t necessary. From a business standpoint there is no need for a 100% correct answer 100% of the time. You just need a “good enough” answer, 90% of the time, to be able to earn more money than you lose. And time-to-market might be much more important than correctness and stability. And there’s nothing inherently bad with this approach. That’s basically how we all earn money and stay alive. In the fields where correctness is a must (like NASA, healthcare, high-frequency trading and other banking operations) they don’t use OOP. you can look at OO programs out there and see for yourself that most of the time they violate even such a basic correctness principle as the Liskov substitution principle (in the sense that overriding methods should obey the contract of their ancestor.” 摘取自《为什么Dijkstra说差劲的OOP可能来自加州》。Dijkstra热衷于编写正确的程序,对于Dijkstra来说OOP可能是一坨屎…… 过程式编程有图灵机,函数式编程有lambda代数,甚至分布式编程也有数学模型支撑,但多少年来,OOP什么都没有。Mayer在《Object-oriented Software Construction》中提出的Design-by-contract无法在现实的项目代码中发挥作用。这位回答者将OOP归结于UI和商业软件对正确性的低要求,以及快速抢占市场的商业策略。有时我们呆坐在那看着眼前的代码,发现有的连最最基本的里氏代换原则都无法满足!
排除掉软件内部的状态,软件的其他变化则来自外界,从用户需求到硬件状态,可能对我们的设计产生影响,也可能产生异常弄垮运行时的系统。绝大多数软件系统都不是静止的,而像一个活着的怪兽。这一点在大型的分布式系统中尤为明显。比如像AWS一样拥有大量用户的云服务,我们没法停机(那样会“杀死”它),同时我们还要不断地完善它,仿佛就像在饲养它一样。如果软件没有变化,过多地讨论设计也就没有了意义。
限于篇幅,对于本文提到的影响软件工程复杂度的三个重要方面:细节、正确性和变化,在此mark一下学习的进度,之后会以单独的文章进行全面的总结。
处理细节最强有力的武器就是抽象了,刚刚重读了SICP(《计算机程序的构造与解释》),在读了很多经典编程著作后,个人觉得目前为止对编程思想,尤其是抽象思想讲解得最好的莫过于SICP了。所以关于抽象化思考,准备参考SICP的前两章,分别从过程和数据两方面,一动一静。
前面读了几位大师对面向对象编程的观点后,悲观的甚至有些绝望。突然觉得如果追求完美和纯粹的话,OOD简直就没法继续用了。其实这个问题可以这样来想:一来要在纯粹与现实之间找个平衡,OOD能流行这么多年肯定是有可取之处的;二来很多更聪明的人肯定早就察觉了OOD的问题,有了更深的了解,所以才有那么多DDD、CQRS等架构和理念。同样地,FP中Stream的概念是值得参考借鉴的,但直到现在它也并没有压倒性地战胜OOD也是有其弊端和局限性的。总之,不妨对OOD有了更深的了解之后再去悲观。关于FP方面这一部分,准备参考SICP的第三章,OOD方面的参考待定。
为了尽可能地减少外界影响,我们首先要做的就是识别、隔离易变化的部分。《Code Complete》中给出一些经验:商业规则、硬件依赖、输入输出、非标准不可移植的语言特性、数据长度等。但有时我们仍然无法避免变化带来的影响,这时就要修改代码,重构代码。《Working with Legacy Code》是这方面的一本好书,如果我们能够做好第一个方面——即抽象出好的设计,再辅以一些遗留代码开发经验和重构技巧,那么我们就不再惧怕变化。