当前位置: 首页 > 科技观察

软件工程的迷茫与沉思

时间:2023-03-19 13:06:17 科技观察

本文转载自微信公众号“码砖杂工”,转载请联系码砖杂工公众号。1960年代爆发的软件危机催生了软件工程。人们希望用工程方法来管理、设计、构建和维护软件。从那以后,聪明的工程师们一直在通往更好软件的漫长道路上努力奋斗。开发语言经历过汇编、C、C++、Java、Erlang、Python;编程范式涵盖面向过程(POP)、面向对象(OOP)、泛型(GP)、函数式(FP);软件架构从单机到分布式,从云原生到云原生,包括单体、库组件模块服务、分层、微服务、MVC/ServiceMesh/Serverless等;而软件工程思想和方法论则包括以生命周期管理为核心、以过程为中心的瀑布模型(WaterfallModel)、以需求演化为核心的注重迭代、渐进的敏捷开发,以及领域驱动设计(DDD:DomainDrivenDesign)以边界划分和控制为核心,注重领域建模。世界是多彩而复杂的,将现实世界映射到虚拟软件中注定不是一件容易的事。软件开发是一门权衡的艺术。例如,快速交付与安全生产往往背道而驰,开发效率与运营效率始终难以匹敌。.所以在漫长的软件开发历史中,人们发明了解决一个问题的方法,几乎??总是引入另一个问题,软件工程师不得不面对这个混乱的世界。面向过程(C)认为一切都是过程,可以将真实世界封装成过程,通过过程连接和安排模拟世界。然而,随着软件被大规模用于解决复杂的业务问题,这种范式已被证明缺乏足够的抽象。虽然函数可以看作是最小粒度的模块化技术,但仍然无法掩盖模块化能力的不足。过程和操作数据的分离也导致软件偏离了高内聚和低耦合的方向。为了解决上述问题,面向对象范式(C++/Java)被设计为通过对象对世界进行建模,对象封装属性和方法,通过开放接口与外界进行交互,为软件提供逻辑模块化的方法设计;此外,对象很容易映射到现实世界的事物。对象通过组合来表示更复杂的概念,通过接口泛化来表达更抽象的概念。泛型的动机更简单。需要一种语言机制来解决跨数据类型提供标准容器的障碍。C++通过模板的语言机制提供了参数化类型、编译时类型检查和类实例化的能力。它不仅保证了类型安全,提高了执行效率,而且增加了编译时间,损害了可读性,尤其是模板元编程等新方法的引入,让事情变得更加复杂。UML诞生于瀑布模型流行的时代。它是一种独立于特定编程语言的面向对象的建模工具。UML将面向对象开发分解为三个阶段:分析(OOA)、设计(OOD)和编码(OOP)。重分析设计,轻编码实现。由于设计和实现分为相互制约的两个阶段,因此在开发过程中会出现两个模型,即表现在UML图纸中的设计模型,隐藏在软件源代码中的实现模型,两个阶段。模型必然导致设计和编码的分离。将设计和实施交给不同的人,看似有利于分工协作,实则增加了沟通成本,降低了交付效率。学院派曾经追求代码可以根据架构师的UML设计图自动生成,看起来很美,但实际上这个目标只有在有限的情况下才能实现;而在越来越复杂的商业软件开发中,工程学院觉得更多的时间消耗在开发和维护上,最终交付的成果只能是源代码而不是图纸,所以开发者只能求助于设计原则(SOLID)和设计模式(GOF)为了舒适,而设计师头顶的“建筑师”美誉是天马行空。瀑布模型的过程很难逆向,只有在项目后期才能看到结果。针对瀑布的缺点,敏捷开发试图改进软件开发端到端的通信方式。极限编程(XP)是一种实现敏捷开发的轻量级方法。层次软件工程方法论,试图螺旋式演进,极限编程面对软件活动的复杂性,承认需求无法在初始阶段固化,提倡开发者优先将精力投入代码,通过引入基本价值观、原则、方法等理念,灵活应对需求变化。敏捷开发在互联网的蓬勃发展中大放异彩。根本原因在于互联网应用的需求是动态变化的,很难严格遵循繁重的传统瀑布模型流程。重新制作原型的敏捷方法甚至被误解为一种完全不需要设计和文档的开发方法。敏捷的优势同时成为它的劣势。忽视文档的重要性,在人员流动较大的情况下,无疑会增加维护的难度,而当面临领域知识的复杂组织和应用时,敏捷开发也受到质疑。随着WebService应用的井喷,以Java为代表的新兴语言正在攻城掠地,Controller->Service->Dao或者SOA设计已经转变为业界的标准方案。Service层充当神类,所有逻辑都在Inside,充满getter/setterDAO的贫血模型臭气熏天,数据驱动开发模式陷入泥潭,领域驱动设计进入人们的领域的愿景。2003年,EricEvans倡导的领域驱动设计(Domain-DrivenDesign,DDD)将“领域核心”视为企业最重要的资产,提倡通过通用语言(UbiquitousLanguage)消除表达的不准确。领域驱动设计继承和发展了敏捷开发。DDD把领域和设计归为软件设计的核心,让业务人员和开发人员得到同样的重视。推荐联合起来捕获拥塞的领域模型。DDD战略设计宏观上决定了限界上下文,而战术设计给出了一些实现层面的最佳实践。传统模型坚持以数据(数据库)为中心的概念,而领域驱动设计则转向以领域模型为中心,这是一种根本性的设计变革。DDD通过四个边界划分问题空间和解决方案空间,并确定核心、通用和支持子领域。在限界上下文内部,通过层级(基础层-领域层-应用层-表现层)实现内外隔离,应用层形成了保护层,有效隔离了业务复杂性和技术复杂性。将领域层视为整个系统的稳定和内聚核心是领域驱动设计的一个关键特征。回顾软件工程发展过程中出现的各种学说,每一种流行的思潮都有一套自圆其说的理论,号称可以完美解决某些问题,但无一例外地落入了另一个框架陷阱,但它们都有一套自我辩护的理论。可以结束软件工程的无序设计。自娱自乐的无休止重构循环的恶性循环无法跳出,我们还处在各种技术债累累的困境中,所以,跳出各种框架模型的心理枷锁,回过头来看,我们或许以及重新审视设计的本质,我们不妨思考一下处理软件复杂性的根本原则。解决大规模复杂软件问题的方法可以简单归纳为几点:抽象、分解和隔离。抽象就是分类,分类是为了重用。抽象的意义是通过表现找到事物背后的本质。抽象的目的是减轻认知负担,避免重复思考和劳动,简化问题空间,使人关注更高层次的事物。建模是完善心智模型的过程。本质是一种抽象。分解就是把一个复杂的问题分解成更小的、易于解决的问题。分解问题的过程就是简化问题的过程。问题分解后,需要协作。这其实就是分而治之的概念。库、组件化、微服务都闪耀着分而治之理念的智慧。拆分方式有两种:技术维度和业务维度。微服务和DDD都是从业务维度拆分问题。分裂可以遵循AKF原则和康威定律。高内聚和低耦合是评价分裂质量的标准。让上帝归上帝,让凯撒的归凯撒。隔离是为了去耦。建立一个松耦合的系统一直是工程师们的目标。分层是实现隔离的有效手段。每一层都专注于自己的功能实现。上层使用下层的能力,下层为上层提供服务。层通过约定的接口进行交互,不相邻的层是完全透明的。外部世界的规则是契约、通信和系统级架构风格和模式,而内部世界的规则是分层、协作和类级设计网格和模式。在软件变革的洪流中,软件工程的先驱先贤们提出了各种编程思想和方法论,但都不能从根本上解决问题。《人月神话》的第16章提出,因为软件工程是一个超级复杂的系统,所以断言没有银弹。不仅没有包治百病的灵丹妙药,而且还指出,未来十年内没有办法将效率提高十倍。回顾历史,每一个完美的解决方案都是从一个现有的解决方案开始,声称可以解决所有问题,然后进行布道,将大众带入自己精心设计的逻辑闭环中,然后追随者用一种宗教的虔诚来传播theory生搬硬套到项目上,最终交付的代码依然充满各种歧义和污秽,任何进化都可能导致偶然不变性的瞬间崩塌,留下一地乱麻,那些新奇的理论终将如卷毛一般。它像烟雾一样,飘荡在历史的浩瀚天空。古人云:人天生愚昧,但不愚昧。是教育使人变得愚蠢。古人还说:学而不思则懒。因此,我们应该意识到投机的重要性。对于知识,我们学习它,研究它,但我们不盲从。那么我们在处理软件工程时应该秉持哪些原则呢?首先,人是关键,软件开发没有终极解决方案,因为软件是人思维的外化,人本身就充满缺陷。我们必须认识到人类抽象过程中不可避免的一些缺陷,以及人类抽象能力的差异。这就意味着,相对于规则和流程,人实际上才是软件实现过程中的灵魂。思想和规则可以为人们提供指导,但不能神奇地解决软件工程中的所有问题。影响软件开发质量的关键因素是人,而不是设计方法。关注形式而不是内容,关注文档而不是交付的代码,是本末倒置。二是实事求是,具体问题具体分析。软件覆盖范围太广,意味着每一个具体的实施规则都有其局限性,不能用死板的标准套住手脚。立为教条,勿以倚天剑剪指甲,勿以龙刀剃须。比如最简单的业务CRUD模型可能就够了,有的可能适合CQRS、六边形架构,有的贫血领域对象也可以,有的可能用事件风暴模式更好。有时候数据和操作要分开,甚至读写也要分开。应该是分开的,有时候把数据和操作封装在一起反而更好,脱离现实杀鸡取卵只会增加笑料。第三,几乎任何语言和技术都有好的一面和不好的一面,都有适用性。比如C,虽然缺乏抽象能力,但其核心语法集非常简单。简单意味着专注和可靠,这意味着对程序员的要求更低,你只需要掌握几十个几十年不变的STDCAPI就可以构建所有的应用程序,但你必须意识到它缺乏抽象能力和开发效率.C++虽然有很好的抽象能力,但是它的语法集太大,似乎很难约束大家在最小的公共知识层面上行动,但是如果能达成共识,或者大家的水平都比较高,写出来的程序通过它,确实具有更好的可维护性。第四,纪律!是的,纪律是关键。一个方法体系再完善,如果团队不能严格执行方法体系规定的纪律,那就是一句空话。无论是干净的编码、架构设计、敏捷开发还是领域建模,只有始终如一地遵守纪律并以纪律施加约束,才能不断提高质量。最后,虽然没有灵丹妙药,但我们也不要过于悲观。软件工程一直在迂回前进。每一步都可以解决大量问题并引入一些可控的副作用。但是,从宏观上看,软件工程仍然伴随着人类。社会在不断进步。那么,为什么不尝试拥抱不完美呢?