新技术虽好,但长期项目中架构腐化问题如何解决?

2024-07-19
来源:网络整理

发件人:陈金洲

前言

新技术层出不穷,在过去的十年中,我们经历了很多让人激动的新技术,包括新的框架、语言、平台、编程模型等等。这些新技术极大地改善了开发者的工作环境,缩短了产品和项目的上市时间。然而作为在软件行业一线工作多年的从业者,我们不得不面对一个现实,那就是采用新技术的乐趣随着项目周期的增加而迅速降低。不管当初的选择有多么光鲜,半年一年之后,只要项目还在活跃,业务在扩大——需要添加的功能越来越多,一些常见的问题就会逐渐显现出来。构建太慢、完成新功能让你苦不堪言、团队成员无法快速融入、文档无法及时更新等等。

架构损坏是如何在长期运行的项目中发生的?为什么常见的面向对象技术无法解决此类问题?我们如何才能减缓架构损坏?

本文将尝试解释这一切,并提出相应的解决方案。读者需要有相当的开发经验——至少在同一个项目上开发一年;公司中负责架构演进和产品演进的人将从本文中找到灵感。

建筑学

架构这个词在各种场合以各种形式不断出现。从上的词条中,我们经常听到插件式架构()、以数据库为中心的架构()、模型-视图-控制器架构(MVC)、面向服务架构(SOA)、三层模型(-Tier)、模型驱动架构(MDA)等等。奇怪的是,这些词越大,对实际开发人员来说就越痛苦。SOA很好——但在它被提出的那个年代,它只为开发人员带来了虚幻的“公共数据类型”;MDA甚至没有机会成为新一轮的可笑的CASE工具。

在继续阅读之前,你或许想问自己一个问题:这些大词真的在长期项目中给你带来过什么好处吗?一个更实际的问题是:作为奋战在一线的开发者,你在长期项目中有过良好的体验吗?

技术的演变和挥之不去的痛苦

企业应用的发展似乎在十年前就已经开始,从 ASP/LAMP(、、、PHP)时代开始,各种企业应用纷纷迁移到浏览器上。经过十年的发展,现在的阵营已经百花齐放。与过去不同的是,现在的技术不仅在编程语言上,在编程的通用套路、最佳实践、方法论、社区上,都是各技术独有的。目前主流的阵营有:

这些新技术没有理由不让人兴奋。它们解决了许多之前存在的问题。它们的网站都吹嘘自己的效率有多高,比如 15 分钟就能创建一个博客应用,2 分钟就能快速上手的教程等等。与过去需要 21 天才能学会 XXX 相比,现在上手要容易得多。

需要气馁的是,本文开头提出的问题就像幽灵一样萦绕在上述任何一种技术之下。工作半年之后,一个10人使用Ruby的高效团队的构建时间从2分钟降低到了2小时;我们曾经使用.NET 3.5(C# 3.0)的一个项目,在生成2万行代码时,构建时间超过半小时;我们的一些客户工作在10年前的Java代码库上——他们竭尽全力让技术栈保持最新:、、、等等,但他们面临的困境是需要同时打开72个项目才能编译进去;因为编译和打包时间太长,他们已经去掉了大部分的单元测试——带来了巨大的质量风险。

如果你真的做过长期项目,就应该清楚,这种痛苦似乎没有任何框架能够从根本上解决。这些新时代的框架解决了大部分显而易见的问题,但对于解决长期项目面临的问题却无能为力。

一步步:建筑如何腐化

无论任何时代的建筑师如何绚丽多彩地描述建筑,正在开发的项目都不会超出以下图景:

基本架构图

一些基本准则:

阶段1

满足这个条件的架构在早期阶段是非常令人愉快的。上一节中我们描述的框架都满足这个架构。这个阶段开发非常快:IDE 打开很快,开发功能完成很快,此时团队往往很小,沟通也没有问题。每个人都很高兴——因为他们使用了新技术,也因为架构如此简单、清晰、有效。

阶段2

美好的时光并未持续多久。

很快你的老板(或客户,随便什么)就有了一揽子想法,可以和这个团队一起实施。工作有条不紊地进行着。添加了更多功能,增加了更多团队成员。新添加的功能也是按照之前的架构进行开发的;新团队成员也对清晰的架构感到高兴,并一丝不苟地遵循它。不久——可能是三个月,甚至更短,你就会发现代码库变成了这样:

正常发育后

您可能很快意识到这有问题。但您很难意识到这意味着什么。常见的操作通常围绕重构 - 提取垂直相关的项目以形成新项目;提取水平相关的项目以形成称为或基础的项目。

无论您进行哪种类型的重构,都会有一些变化悄然发生(可能只是变快或变慢)。构建过程不可避免地会变得更长。一开始一两分钟就变成了几分钟,然后是十多分钟。通过重构构建脚本并删除不需要的部分,构建时间会缩短到几分钟,您会感到满意,因此可以继续。

第 3 阶段

功能越来越多,加入的人也越来越多。构建时间又变长了。随着代码加载越来越多,IDE 的速度越来越慢;沟通也越来越多——不是每个人都能理解所有的代码。有一次,一位非常有道德的程序员试图重构一些重复的逻辑,但发现涉及的代码太多,而且很多是他不懂的业务问题,所以他放弃了。越来越多的人这样做,代码库变得越来越臃肿,最终没人能弄清楚系统是如何工作的。

系统继续在混沌的状态下慢慢变得混乱——这个过程比写这篇文章的时间要长得多,而且还会有重复,但从我的观察来看,在不超过1年的时间内,无论采用什么技术框架、应用什么架构,这个过程似乎都是无法抗拒的命运。

常见解决方案

我们没有坐等问题发生,在问题被发现之前,我们优秀的同事就采取了各种解决方案,常见的解决方案如下:

升级工作环境

没有什么比一台与时俱进的电脑更能激励开发者了。最多每三年升级一次开发者的电脑——升级到当时最好的配置,可以大大提高生产力,激励开发者。另一方面,使用过时的电脑,在速度慢的机器上开发,不仅客观上会降低开发效率,也会让开发者在心理上变得懒惰。

升级的工作环境不仅仅与计算机有关,还与您工作的空间有关。良好的、有利于沟通的空间(和工作方式)有助于发现问题并减少问题的发生。隔断不适合发展。

分阶段建设

一般来说,构建的顺序是:本地构建确保所有功能正常运行,然后提交并等待持续集成正常工作。本地构建超过 5 分钟就会变得难以忍受;大多数情况下你都希望这个反馈时间尽可能短。在项目早期,经常会运行所有步骤:编译所有代码,运行所有测试。随着项目周期变长,代码增多,时间会越来越长。在尝试重构构建脚本数次而无法再优化之后,“分阶段构建”成为大多数人的选择。通过合理的拆分和分层,每次运行特定的步骤,比如只运行特定的测试、只构建必要的部分;然后提交并让持续集成服务器运行所有步骤。这样,开发人员就可以继续进行后续工作了。

分布式构建

即便本地速度提升,使用分阶段构建的团队很快发现 CI 服务器的构建时间越来越不尽人意,每次提交后半小时才能得到构建结果,这让人无法接受。各种各样的分布式技术应运而生,除了常见的 CI 服务器自身提供的能力外,很多团队也发明了自己的分布式技术,他们往往能够将代码分发到多台机器上编译、跑测试。这种方案可以在相对较长的时间内有效——当构建速度变慢时,只需要调整分发策略,将构建过程跑在更多的集群机器上,就能显著减少构建时间。

使用或

一些新工具可以显著加快开发人员的工作速度。它们可以把需要编译的 Java 语言变成修改保存后立即生效的东西,减少了大量修改、保存、重新编译和部署的时间;它们可以启动一个测试,并将测试相关的代码缓存在其中,这样在运行测试时,就不需要重新加载了,大大提高了效率。

问题是什么?

上述方案在特定时间域内,能够很好的解决部分问题,但项目运行一年、两年甚至更长时间后,仍然无法避免构建时间变长、开发速度变慢、代码混乱、架构模糊、新手上手困难等问题。问题的关键究竟在哪里?

人们喜欢简单。但这似乎是个谎言——没有多少团队能从头到尾保持简单。人们喜欢简单只是因为它很难做。并不是人们不想做。很多人都知道,软件开发与其他劳动密集型行业没什么不同——人越多,产出越大。《人月神话》曾提到,项目增加人手,在增加工作产出的同时也会产生混乱。短期内,这种混乱可以被团队以各种形式消化;但从长远来看,随着团队成员的变化(新人加入,旧人离开),以及人们正常而自然的遗忘曲线,代码库会逐渐失控,混乱无法消化,项目就停不下来,新功能不断加入,架构就会一天天被侵蚀。

人的认识总是有极限的,而需求和功能却没有极限——今天的功能总是比昨天的多;这个版本的功能总是比上一个版本多。在长期的开发中,忘记以前的代码是正常的;忘记某些约定也是正常的;形成一些细小的、不经意的错误是正常的,在庞大的代码库中这些小错误被忽略也是正常的。这些不断积累的小不一致和错误最终会随着时间的积累变得难以控制。

很少有人注意到,规模的增大才是建筑败坏的根本原因——时间和空间上因果的不连续性意味着人们无法从中获得经验,而只能一次又一次地重复这种悲剧的循环。

解决方案

解决方案的最终目的是在混乱发生之前、在我们的认知变得受阻之前,控制好项目的规模。这并不容易,大多数团队都承受着相当大的交付压力。大多数业务用户没有意识到,毫无节制地向项目/产品添加需求只会导致产品的崩溃。看看就知道,到最后产品会变得多么混乱、多么难用。我们这里主要讨论的是技术方案,在业务上,也需要时刻警惕需求的增长。

开发微信小程序商城的流程_微商城小程序app开发_django本地开发微信小程序商城

1. 采用新技术

这或许是最便宜、最容易的解决办法。新技术往往是为了解决某些特定问题而诞生的,往往是经验的集合。学习和了解这些新技术可以大大减少过去实现某些技术目标所必需的经验积累过程。就像武侠小说里经常有奇遇、突然获得隐居多年的内功的主角一样,这些新技术可以迅速帮助团队摆脱某些特定的痛点。

有足够多的例子可以证明这一点。在 J2EE 出现之前,开发人员只能按照 J2EE 模式文档中的各种实践来构建自己的系统。有一些简单的框架可以帮助这个过程,但总的来说,在处理今天看来很基础的方面,比如数据库连接、异常管理、系统分层等,仍然需要做大量的手工工作。在 J2EE 出现之后,你不需要花费太多的精力,就可以很快得到一个具有良好系统分层和大多数设施就绪的基础。这有助于减少代码库的大小并解决可能出现的低级错误。

另一个极端的例子,它不仅给开发带来了便利,还带来了人们在业界多年的部署经验。数据库、+或者+,这些过去看似极其复杂的技术,在市场上已经变得微不足道——任何懂一点命令行的人都可以部署它们。

没有哪个组织能够独占所有这些新技术。因此,作为一名软件从业者,你需要持续关注技术社区。孤立地工作只会加速架构的衰败——尤其是当这些发明已经在开源社区中成熟时。那些看似光鲜的产品背后,其实有无数的失败案例和成功经验支撑着它们。

我们曾经有一个项目,在意识到需求可能会转向类似密钥的文档数据库后,团队大胆尝试采用 2008 年的 XML 功能,在 SQL 内部实现类似 No-SQL 的数据库。这是一个新发明,创造者一开始很兴奋,终于有机会做点不一样的事情了。然而,随着项目的进展,越来越多的需求出现了:支持、监控、管理工具支持、文档、性能等等。随着项目的进展,最终发现这些功能与当前流行的功能如此相似——大部分问题都已经解决了。此时,代码库已经相当庞大——这部分代码让很多团队成员感到困惑;一年后,只有大约 2 个人能够理解它的实现过程。如果早点采用,团队就有机会放弃大部分相关工作。

值得一提的是,自大的开发者往往对新技术缺乏耐心;或者缺乏耐心去了解新技术的能力或局限性。每个产品都有自己的问题域,新技术往往还不够成熟,无法应对问题域之外的问题。开发者需要不断阅读、思考、参与,以验证自己的问题域是否与之匹配。一知半解就止步不前并不是一个好的态度,也会阻碍新技术在团队内部的推广。

新技术的选取往往发生在项目 / 产品的特定阶段,比如起步阶段,或者某个痛点期。在日常阶段,开发者依然需要时刻关注代码库。下一步,重构为物理隔离的组件,是应对日益增长的代码库的另一种解决方案。

2. 重构为物理隔离的组件

明显的趋势是,对于同一个产品,需求总是在增加。去年有 100 个功能,今年有 200 个。去年有 10 万行代码,今年可能有 20 万行。去年一台 2G 内存的机器就足够进行正常开发,今年似乎需要两倍。去年有 15 名开发人员,今年有 30 名。去年构建最多需要 15-20 分钟,今年则需要一小时,并且必须进行分布式构建。

有些人会注意到代码的设计问题,并孜孜不倦地进行重构;有些人会注意到构建缓慢的问题,并孜孜不倦地改善构建时间。然而,很少有人注意到代码库的扩大才是问题的根源。很多常规的策略往往针对组织:比如按照功能模块(如ABC功能)或者按照层级(如持久层、表现层)划分代码库,但这些分割的项目依然存在于开发人员的工作区中。无论项目如何组织,开发人员都需要打开所有项目才能完成编译和运行过程。我曾见过一个团队需要打开120个项目;我自己也经历过需要打开72个项目才能完成编译的情况。

解决方案就是物理上隔离这些组件,就像团队使用 //Asp.NET MVC/ 等库时,不需要将其对应的源代码放入工作区进行编译,也可以将稳定的工作代码单元组织到对应的库中,标记版本并直接引用二进制文件。

在不同的技术平台上有不同的解决方案,Java世界有一个由来已久的库,可以很好地管理不同版本的JAR及其依赖项;.NET有点遗憾,在这方面没有真正成熟的东西——不过有了参考实现,团队自己搭建一个也不难(可能更难的是与的集成);Ruby/世界有大名鼎鼎的gem/系统,不要把自己整理出来的相对独立的模块放到/lib/中,整理出来,形成新的gem,引用它(团队需要搭建自己的gems库)。

同时代码库也需要进行大刀阔斧的重构,之前的代码结构可能如下(这里以 SVN 为例,因为 SVN 有清晰的 //tags 目录结构,Git/hg 类似)

原始库结构

改进之后就会是下图这样:

改进的库结构

每个模块都有自己的代码库、自己独立的升级和发布周期,甚至自己的文档。

这种方案看似容易理解,实际操作起来却很难。一个团队运行久了之后,很少有人关心模块之间的依赖关系。一旦拆出来,分析已有的几十上百个项目之间的依赖关系就相当困难了。最简单的处理方式就是查看代码库的提交记录。比如最近三个月没人提交过某个模块,那么这个模块基本就可以拿出来,形成二进制依赖。

很多开源产品都是通过这个过程形成的,例如(请参考《J2EE 设计、开发和编程指南》,Rod 基本上解释了整个设计思想的来源)。一旦团队开始这样思考,每隔一段时间重新审视代码库,你就会发现核心代码库不太可能失控,你也会得到一套设计良好、稳定的组件。

3. 将独立的模块放入独立的进程中

上述方案只有一个核心原则:始终将核心代码库保持在团队可理解的范围内。如果效果好,可以很大程度上解决代码规模增长导致的架构腐败问题。但该方案只是在静态层面解决了系统的隔离性。随着隔离的模块越来越多,系统运行时需要的依赖也越来越多。这些依赖在运行时分为两类:一类是类似//,是系统运行的基础,运行时必须存在;另一类是相对独立的业务功能,比如缓存读取、电商支付模块等。

第二种依赖关系可以更进一步:放在一个独立的进程中。在如今一定规模的系统中,登录和注销功能已经从应用中分离出来,要么使用SSO方案进行登录,要么干脆委托给另外一个登录系统。在开发过程中,团队发现缓存的读写其实可以放在一个独立的进程中(而不是直接运行在运行环境中的类似方案),于是就发明了现在大名鼎鼎的。在之前的一个项目中,我们发现支付模块可以完全独立出来,于是就把它隔离出来,通过REST形成了一个新的、无接口的、始终运行的处理支付请求的系统。在另一个发布项目中,我们发现编辑和编写报表的过程其实是独立于报表发布过程的,虽然有类级别的复用,但是是在业务层面的。最终,我们将报表发布过程做成了一个常驻服务,系统其他模块通过MQ消息与其交互。

这个方案应该不难理解,和方案一不同,这个方案更多的是从业务的角度去思考系统。由于系统会把这个模块运行在独立的进程中,所以在不同的进程中可能会存在一些代码重复的情况。比如说,如果两个不相关的项目同时存在,大家都觉得没什么大不了的;但如果一个属于自己的业务组件同时在同一个项目的两个进程中重复出现,很多人就会觉得难以接受。(题外话:这种干净在OSGi环境中也存在)这里需要提醒的是,当它们处于不同的进程中时,在物理上和运行时是完全隔离的。整个架构都要从进程的角度去考虑,而不是简单的物理结构。

从单进程模型到多进程模型的架构思维转变也并非易事——需要架构师有意识地加强这种实践。流行的.NET和Java世界倾向于把所有东西都放在一起。世界/可以更好地平衡优秀产品之间的进程协调。例如,使用。另外,现在多核环境越来越多。与其在编程语言层面花费大量精力,不如享受多核带来的好处。多进程可以简单而显著地利用多核能力。

4.形成高度松耦合的平台+应用

现在我们把眼光放得更远一些。想象我们正在构建一个类似于开心网、人人网和 的系统。它们的共同特点是可以接入几乎无限的第三方应用,无论是买卖好友这种简单的应用,还是各种华丽的社交游戏。令人惊叹的是,要实现这一点,并不需要第三方应用的开发者使用和它们一样的技术平台,也不需要服务器提供无限的计算能力——大部分架构都由开发者控制。

在企业应用中实现这一点并不难,秘诀在于当用户访问第三方应用时,其实是通过后端访问第三方应用,将当前用户的信息(以及好友信息)通过 HTTP POST 发送到第三方应用指定的服务 URL,再将应用的 HTML 结果渲染到当前页面。从某种意义上来说,这项技术本质上是服务端的。(详情可参考文章)

应用程序架构

这种架构的好处是极其分布式,一个外表看起来一致的系统,其实是由若干个耦合度极低、技术架构完全不同的小应用组成,它们不需要部署在同一台机器上,可以单独开发、升级、优化。一个应用的瘫痪,不影响整个系统的运行;各个应用的自我升级,对整个系统没有影响。

这不是终极方案,只在某些特定条件下有效。当系统规模非常大,比如由几个子系统组成,接口基本相同,子系统之间连接很少,基于这个前提,可以考虑采用这种架构。抽象出极少量真正有效的通用信息,系统之间使用HTTP POST即可。其他系统可以独立开发部署,甚至可以根据应用访问情况做特定的部署优化。如果不这样做,一个系统就会堆积数百万行代码,久而久之,开发人员就会逐渐失去对代码的控制,架构的腐化就是时间问题了。

例如,某银行的财务系统包含十几个子系统,包括工资、资产、报表等模块,每个部分都相对独立且复杂,如果将整个系统这样拆分,可以实现单点优化,而不需要重启整个应用。对于每个应用,开发人员可以在更小的代码量内使用熟悉的技术方案,从而降低架构崩溃的可能性。

结论 没有糟糕的架构,只有变化才会产生糟糕的架构

我访问过很多团队,很多项目开始的时候,他们花了很多时间选择用哪种技术体系、架构,甚至用哪种 IDE。就像小孩子选择自己喜欢的玩具一样,我相信不管流程是怎样的,团队最终都会开心地选到自己选择的东西,并坚信自己的选择没有错。事实也确实如此。项目开始的时候很难有真正的架构挑战。难就难在,时间长了,人会忘记;很多人加入,在完成新功能的同时,还需要理解旧代码;代码量的每一次突破,都会引起架构上的不适;这些不适包括:新功能引入变得困难,新人很难快速上手;构建时间变长等等。这些能不能警示团队,采取结构性的解决方案,而不是临时性的?

关于本文档

很多人说 不提倡文档。他们说文档很难写。他们说开发人员不会写文档。所以没有文档。

奇怪的是事实并非如此。写出好程序的人也擅长写作。世界上绝大多数人都是程序员,其中许多人的写作能力很强。

项目中的文档往往少得可怜,新人总是一头雾水,新人看一两天或者学习一下就能很快了解这些工具的使用,但加入团队的时候却没有任何文档,这很奇怪。

抛开项目持续运行和交付的特性不谈,我认为庞大、不稳定的代码库是文档快速失效的根本原因。如果我们能按照上述解决方案减少代码库,那么独立的模块或应用程序将有机会在更小的范围内拥有更多独特的价值。想象一下现在的 /,它们往往有超过 20 个第三方依赖,但我们并不觉得难以理解。最重要的原因是依赖被隔离后,这些模块有独立的文档可以学习。

企业级项目也是如此。

创建一个应用程序生态系统,而不仅仅是一个单一的项目

同一个产品不断新增功能并不奇怪,但通过前面的分析,我们应该重新思考这个常识,是打造一个越来越庞大、越来越慢、越来越没有生命的产品,还是将其有机分解成一个有不同依赖关系、生机勃勃的生态系统?项目参与各方(包括业务用户、架构师、开发者)都应该摆脱短视,专注于打造可持续发展的应用生态。

【今日微信公众号推荐↓】

分享