软件工程的迷途和沉思
上世纪60年代爆发的软件危机催生了软件工程,人们寄希望于借助工程化的手段管理、设计、构建和维护软件,自此,聪明绝顶的工程师便在追求更美好软件的漫漫长路上艰苦求索。
开发语言经历了汇编、C、C++、Java、Erlang、Python;编程范式涵盖了面向过程(POP)、面向对象(OOP)、泛型(GP)、函数式(FP);软件架构从单机到分布式到云原生,包括巨石,库组件模块服务,分层,微服务,MVC/ServiceMesh/Serverless等;而软件工程思想和方法论则包括以生命周期管理为核心注重工序的瀑布模型(Waterfall Model),以需求进化为核心注重迭代渐进的敏捷开发(Agile Development),以边界划分和控制为核心注重领域建模的领域驱动设计(DDD:Domain Driven Design)。
世界是缤纷复杂的,要把真实世界映射到虚拟软件注定不会是一件容易的事,软件开发是权衡抉择的艺术,譬如快速交付和安全生产常常背道而驰,开发效率和运行效率总是难以一致。所以在软件发展的历史长河中,人们发明一种方法解决一个问题,而几乎总是会引入另一个问题,软件工程师不得不面对混沌不堪的世界。
面向过程(C)认为一切皆过程,现实世界都可以封装为一个个过程,通过过程串联和编排模拟世界。但随着软件被大规模用于解决复杂的商业问题,这种范式被证明缺乏足够的抽象,虽然函数可视为最小粒度的模块化技术,但依然无法掩盖其模块化能力的不足,过程和被操作数据分离也导致软件偏离高内聚低耦合的方向。
为了解决上述问题,面向对象范式(C+++/Java)被设计为通过对象建模世界,对象把属性和方法封装在一起,通过公开接口与外界交互,为软件设计提供一种逻辑层面的模块化手段;而且,对象与现实世界的事物很容易映射。对象通过组合表征更复杂的概念,通过接口泛化表达更抽象的概念。
泛型的动机则更加简单,需要一种语言机制,为解决跨越数据类型而提供标准容器的障碍,C++通过模板这种语言机制提供了参数化类型的能力,编译期的类型检查和类实例化既保障类型安全又提升执行效率,但也增加编译时间和损害可读性,特别是模板元编程等新玩法的引入则让事情更加复杂。
UML诞生于瀑布模型大行其道的时代,是独立于具体程序设计语言的面向对象建模工具,UML把面向对象开发分解为分析(OOA)、设计(OOD)和编码(OOP)三个阶段,该流程注重分析设计而轻视编码实现。
由于设计和实现被划分成2个相互钳制的阶段,所以开发过程会存在两个模型,即一个显化于UML图纸中的设计模型,一个隐匿于软件源码中的实现模型,两个阶段两个模型必然导致设计和编码的割裂,设计和实施交予不同人实施看似有利于分工协作,实则增加了沟通成本,降低了交付效率。
学院派一度追求依照架构师的UML设计图就能自动生成代码,这看起来很美,而实际上这种目标只在受限的情况下才能被满足;而在日益复杂的商业软件开发中,工程派感受到时间更多被消耗在开发和维护上,最终的交付件只能是源码而非图纸,所以开发人员只能转向设计原则(SOLID)和设计模式(GOF)寻求慰藉,设计人员则头顶“架构师”的美名天马行空。
瀑布模型的过程难以逆转,且只有到项目后期才能看到结果,针对瀑布的缺陷,敏捷开发试图从改善软件开发端到端的沟通方式入手,极限编程(XP)是一种实施敏捷开发的轻量级软件工程方法学,尝试用一种螺旋式的方式演进,极限编程正视软件活动的复杂性,承认需求在起步阶段无法固化下来,主张开发人员应该优先将精力投入到代码中,透过引入基本价值、原则、方法等概念来灵活应对需求变更。
敏捷开发在互联网的蓬勃发展中大放异彩,究其根本是因为互联网应用的需求是动态变化的,难以严格遵照沉重的传统瀑布模型流程。
重原型实现的敏捷方法甚至被曲解为完全不需要设计和文档的开发方式,敏捷的优势同时又成为它的弊端,忽视文档的重要性,在人员流动大的情况下无疑会加大维护的困难,且它在面对领域知识复杂的组织和应用时,敏捷开发也饱受质疑。
随着Web Service的应用井喷,以Java为代表的新兴语言攻城拔地,Controller->Service->Dao或者SOA设计摇身变化成行业的标准解,Service层扮演着上帝类,所有的逻辑都往里塞,充斥getter/setter DAO的贫血模式弥漫着恶臭味,基于数据驱动的开发模式陷入了泥潭,领域驱动设计进入了人们的视野。
2003年,Eric Evans提倡的领域驱动设计(DDD)视“领域内核”为企业最重要资产,主张通过通用语言(Ubiquitous Language)消除表达的不准确性,领域驱动设计继承并发展了敏捷开发。DDD把领域和设计归为软件设计的核心,让业务人员和开发人员得到同样的重视,建议合力捕捉充血的领域模型。DDD战略设计从宏观上确定限界上下文,而战术设计在实现层面给出一些最佳实践。
传统模式秉持以数据(库)为中心的理念,而领域驱动设计则转向以领域模型为中心,这是根本性的设计转变。DDD通过四重边界划分问题空间和解空间,确定核心、通用、支撑三类子领域,在界限上下文内部,通过分层(基础层-领域层-应用层-展现层)实现内外隔离,应用层形成了一种保护层,有效地隔离了业务复杂度与技术复杂度。将领域层作为整个系统稳定而内聚的核心,是领域驱动设计的关键特征。
回顾软件工程发展过程中涌现的各种主义,每一个流行的思潮都有一套自圆其说的理论,都声称完美解决了某些问题,但又无一例外的陷入另一个框架陷阱,但又都无一能够终结软件工程的无序设计。我们无法跳出自娱自乐般无休止重构循环的怪圈,我们依然置身于充满各种技术债的困境,所以,跳出各种框架模式的精神枷锁,回过头来,我们不妨重新审视设计的本质,我们不妨想一想应对软件复杂性的根本原则。
可将解决大规模复杂软件问题的方法,简单归为几点:抽象,分解,隔离。
抽象就是归类,归类是为了复用。抽象的意义是通过表现找到事物背后的本质,抽象的目的是为了减轻认知负担,避免重复思考和劳动,精简问题空间,让人关注更高层次的事物,建模是提炼心智模型的过程,本质就是一种抽象。
分解是把一个复杂问题分割为更小的易于解决的小问题,拆分问题的过程即是简化问题的过程,问题分解之后,还需要协作,这其实就是分治的理念,库、组件化、微服务无不闪烁着分治理念的智慧的光芒。拆分有两种方式:技术维度和业务维度,微服务和DDD就是从业务维度做问题拆分。拆分可以遵循AKF原则和康威定律,高内聚低耦合是评价拆分好坏的标准,让上帝的归上帝,让凯撒的归凯撒。
隔离是为了解耦,建立松耦合的系统一直是工程师们孜孜以求的目标,分层是实现隔离的有效手段,每层专注于自己的功能实现,上层使用下层的能力,下层为上层提供服务,上下层之间通过约定的接口交互,不紧邻的层之间完全透明。外部世界的规则是契约、通信以及系统级别的架构风格和模式,而内部世界的规则是分层、协作以及类级别的设计分格和模式。
在软件变革的滚滚洪流中,软件工程的先驱和贤哲们,提出了各种各样的编程思想和方法论,但无一从根本上彻底解决问题,《人月神话》第16章提出,因为软件工程是超级复杂的系统,所以断言没有银弹,不仅没有包治百病的灵药,更指出在未来十年不可能有提升十倍效率的方法。
回顾历史,每一种完美方案都从怼已有的方案和宣称解决所有问题开始,然后传播布道,把大众带入自己精心设计的逻辑闭环,然后追随者以一种宗教般的虔诚,将理论生搬硬套到项目中去,最后交付的代码依然充斥各种模糊不清、污浊不堪,任何演进都可能便引起偶然不变性的瞬间坍塌,剩下一地鸡毛,而那些新颖的理论,最终都会像袅烟一样,飘散在历史的浩瀚天空中。
古人云:人生而无知,却并不愚蠢,是教育使人愚蠢。古人又云:学而不思则怠。所以,我们应该意识到思辨的重要性,对于知识,我们学习它研究它,但不盲从它。那对待软件工程,我们应该秉持怎样的原则呢?
首先,人是关键,软件开发没有终极解,因为软件就是人思想的外化,而人本身充满缺陷。我们必须认可人这种生物在抽象过程中的一些必然缺陷,以及人抽象能力的差异,这将意味着,相比于规则和流程,人其实才是软件实施过程中的灵魂,思想和法则可以给人提供指引,但它们无法神奇的解决软件工程中的所有问题,影响软件开发质量的关键因素是人,而不是设计方法。注重形式而不是内容,注重文档而非交付的代码,都是本末倒置的。
其二,实事求是,具体问题具体分析,软件涵盖的范围实在太广的,这就意味着每一种具体实施细则都有它的局限性,不能用僵化的标准困住手脚,不可拘泥于规则而使之成为教条,不可用倚天剑剪指甲,不要用屠龙刀剃胡子。比如最简单的业务CRUD模型可能就够了,而有些可能适用CQRS、六边形架构,有些贫血领域对象也可以,有些可能事件风暴模式更好,有时候数据和操作应该分离,甚至读写也应该分离,而有时候数据和操作封装在一起更好,脱离实际的牛刀杀鸡只会徒增笑耳。
第三,几乎任何语言和技术都有好坏的两面,都有适用性,比如C,它虽然欠缺抽象能力,但是它的核心语法集非常简单,简单意味着聚焦和可靠,意味着对程序员的要求更低,你只需要掌握几十年不变的少数几十个STD C API便能构建所有应用,但你必须认识到它在抽象能力和开发效率上的不足。而C++虽然有良好的抽象能力,但它的语法集太庞大,而且似乎很难约束大家都在最小的公共知识面行事,但如果能够达成共识,又或者大家水平都比较高,它编写的程序确实有更好的可维护性。
第四,纪律!对,纪律才是关键,一套方法体系不管有多么的完美,如果团队不能严格地执行方法体系规定的纪律,都是空谈。无论是整洁编码还是架构设计还是敏捷开发还是领域建模,只有持之以恒的一致性的遵守纪律,用纪律施加约束,才能持续改进质量。
最后,虽然没有银弹,但我们不应该过于悲观,软件工程一直在迂回中前进,每进一步,就能够解决一大片问题,同时引入一些可控的副作用,但宏观上来看,软件工程还是伴随人类社会在不断进步。
所以,何不尝试接受不完美?