架构是一种负担 简介 与整个公司共享数据库 围绕业务软件构建系统 数十个应用程序之间紧密耦合 在别人的项目上构建自己的项目 所有业务逻辑都在规则管理引擎中 结论

2025-06-10

建筑是一种负担

介绍

与整个公司共享数据库

围绕商业软件构建您的系统

数十个应用程序之间的紧密耦合

在别人的项目上构建你的项目

所有业务逻辑都在规则管理引擎中

结论

更新:法国人可以在这里找到我在巴黎 Java 用户组受本文启发所做的演讲

介绍

在我被派去执行各种任务期间,我曾参与过各种存在各种缺陷的遗留软件项目。

当然,软件质量差(缺乏单元测试、未遵循代码整洁原则……)通常是一个主要问题,但在项目早期,甚至在企业系统初期做出的架构决策也会带来一些问题。在我看来,这类问题是许多项目最大的痛点。

事实上,改进代码相当容易,尤其是在软件工艺运动正在各个团队中推广良好实践的当下。但改变系统的核心,即在其生命周期之初施加的限制,却极具挑战性。

我将讨论我遇到的几种类型的架构决策,这些决策可能会给维护这些系统的团队带来真正的负担。

与整个公司共享数据库

这可能是我见过的最常见的问题之一。当多个应用程序需要使用公共数据时,为什么我们不能简单地共享数据库呢?毕竟,在软件开发中,重复总是件坏事,对吧?嗯,并非每次都这样,尤其是在涉及数据库的时候。Venkat Subramaniam 说过这样一句话,令人难忘:“数据库就像牙刷,你永远不应该共享它”。共享数据库有什么错呢?事实上,有很多原因……

我首先想到的显然是数据模型中的耦合。假设有两个应用程序 A 和 B 都在处理汽车问题。应用程序 A 由负责维修的团队使用,因此他们需要存储大量关于汽车机械性能、故障记录、维修历史等技术数据……应用程序 B 用于处理技术团队的预约,因此它只需要汽车的基本信息就能识别汽车。在这种情况下,两个应用程序使用相同的数据结构毫无意义:它们使用的数据截然不同,所以应该使用各自的数据结构。由于汽车很容易识别,因此无需共享相同的引用,因此更容易实现这一点。

第二个问题也源于数据模型的耦合。假设B想要重命名汽车的标识符,因为从领域角度来看,这样更合理。在这种情况下,A也应该更新以处理新的列名……因此,为了避免打扰A的团队,B的开发人员会开始在另一列中复制信息,因为他们无法更改现有名称……当然,A会说他们会在未来计划这些更改,以避免出现两列包含相同数据的情况,但我们都知道这很可能永远无法实现……

当应用程序不仅从同一来源读取数据,还修改数据时,情况会变得更加糟糕!在这种情况下,谁是数据的所有者?谁应该被信任?如何保证数据的完整性?当同一应用程序的多个部分修改相同信息时,这已经很困难了,而当涉及到多个应用程序时,情况会变得更加糟糕……

我见过的最后一个案例是两个应用程序共享相同的数据结构来存储两个相对接近的业务对象的信息,但它们之间存在一定的差异,这使得理解哪些数据属于哪个应用程序变得非常困难。在这种情况下,两个应用程序都使用同一个表来模拟金融市场执行,但聚合级别不同。没有任何迹象表明该表中存在两种类型的数据,因此我们不得不查看另一个表(属于第二个应用程序)来识别每个应用程序生成的行……每个必须处理该表的新开发人员都不可避免地会像他们的前任一样掉入同样的陷阱,使用不正确的(合理的)数据,这将给公司带来所有风险。

围绕商业软件构建您的系统

并非每家公司都能开发出一套系统来处理所有业务用例。事实上,在很多情况下,这无异于重新发明轮子,因为这些用例对许多公司来说都很常见,而且市面上很容易找到支持这些用例的软件。

所以,购买产品通常比自行开发更便宜。当然,你刚买的软件无法与你正在使用的其他软件集成,所以你需要开发一个连接器,连接两个(大多数情况下是专有的)应用程序。你可能会构建自己的工具来处理特定的业务部分,而由于你购买的这个昂贵软件已经拥有一个便捷的模型,你可能会倾向于直接使用它的数据库,并将你的信息添加到它自己的表中……

几年过去了,几十个开发人员或团队也做了同样的事情,然后你就陷入了困境:如果其他软件的编辑器关闭,或者产品不再受支持,或者其他新产品更符合你的需求,你就无法使用其他软件。在某些情况下,你甚至可能对外部软件产生技术依赖。如果解决方案的编辑器希望你使用每个版本的语言/框架/服务器/等等,那么你就不再拥有自己系统的架构。如果他们想卖给你一个新版本来提供你绝对需要的功能,但如果这个版本意味着技术要求的变化,你就不得不更新所有的技术栈以符合他们的建议。我经历过这种情况,这不是你想经常面对的强制迁移……

我曾经参与过一个项目,当时使用的软件的编辑人员不愿为所有客户开发新功能,因为处理并发修改和多个当前版本(每个客户都有一个特定版本,其中包含他们自己需要的功能)对他们来说太复杂了。于是,他们决定向我们出售一个软件开发工具包 (SDK),以便我们实现自己的功能。当然,他们并没有提供太多关于如何操作的文档,而且我们不得不使用他们的业务实体,由于我们既没有源代码也没有文档,我们需要对其进行反编译才能理解它们的结构……即使是最简单的功能也要花上几天时间才能实现,而且由于一切都非常复杂,并且在已经很复杂的技术栈中引入了团队中无人知晓的脚本语言,因此几乎无法测试……

数十个应用程序之间的紧密耦合

回想一下 21 世纪初,使用企业 Java Bean (EJB) 处理信息系统中应用程序之间的远程调用是多么令人欣喜。在那个时候,这或许看起来是个好主意。与其他团队共享代码库以避免重复似乎也没什么问题。没错,每个团队都被迫同时交付他们的应用程序,以确保没有损坏的二进制依赖关系,但那些夜晚真是令人愉快,一边和同事们一起吃披萨,一边等待耗时 2 小时的交付流程完成,不是吗?

嗯,其实也没那么好玩。而且,因为公司里有人喜欢你的代码,决定把它用在他们未经测试的应用程序中,导致你无法在自己的代码库中重构一个类,这本身就不是什么乐趣。

一旦您意识到这些早期决策所造成的混乱,您就会发现将您的应用程序与世界其他地方解耦所需的努力是巨大的。将您的项目拆分成不同的组件确实需要数年时间,这样其他应用程序将无法再使用您的核心域、客户端或缓存机制,删除所有与其他项目紧密耦合的外部类的使用,用 REST API 替换所有 EJB 调用......但对于参与项目的每个人来说,回报是巨大的:更容易开发和测试,更快的交付过程,因为不再需要与其他人同步,更好地分离您自己的代码中的关注点,更容易的依赖关系管理,不再有传递依赖的问题,因为您在类路径中导入了大量其他应用程序的依赖项......这些昂贵的变化确实为团队带来了生命,并且在项目开始时实施它们会便宜得多!

在别人的项目上构建你的项目

这个问题可能是你最不可能遇到的,但它仍然有可能发生,而且这是最糟糕的情况,因为它会累积之前的几个问题。事实上,我在职业生涯中参与的第一个项目中就遇到过这个问题。

我刚加入项目时,被告知公司系统需要完全重写,而且项目才启动两个月。所以,当我看到一个复杂的Web应用程序,它包含完整的管理模块、已经实现的复杂业务功能,以及一个成熟的框架来帮助开发其他模块时,我大吃一惊。我很快发现,所有这些内容大部分都不是团队开发的:为了避免从头开始,团队决定复用集团内另一家公司开发的框架。问题在于,这个框架并没有与它所开发的项目隔离开来。所以,我们团队拿到的只是一份包含对方公司项目所有源代码的档案,包括他们的业务代码,而这些代码与我们自己的业务毫无关联。更糟糕的是,我们还继承了他们的数据库模式和数据……

作为团队的新人,很难弄清楚哪些代码与框架、我们的项目以及对方公司的业务相关。团队想要清理这些乱七八糟的东西,但由于代码各部分之间存在依赖关系(我没法谈论模块,因为只有一个!),许多尝试都以严重的回归问题告终,而且当然也没有任何自动化测试。此外,我们不得不放弃使用其他应用服务器的想法,因为系统中到处都有对方公司使用的代码,这使得这次迁移对于我们这个小团队来说成本太高了。

有一次,我们想在框架中添加一些很棒的功能,但被告知另一家公司已经完成了。于是,他们要求我们将当前版本与另一家公司的版本合并……团队设法避免了这场噩梦,只挑选了一部分新功能,但它仍然比我们需要的复杂和丰富得多……

我们设法完成了这个项目,但项目质量实在令人头疼。至少有 40% 的代码和数据库内容毫无用处,清理这些死代码从未成为优先事项。希望自从我离开团队以来,团队终于有机会独立完成自己的代码!

所有业务逻辑都在规则管理引擎中

将部分业务逻辑放入规则管理系统是一种常见的做法。例如,当某些业务规则需要频繁更新,但单体应用的交付流程需要经过漫长的测试阶段才能验证候选版本时,这种方法就非常有用,因为这样就无法调整一些“易变”的规则。虽然我更倾向于将所有业务规则都放在代码中,但我也理解有时规则管理系统也能有所帮助。

但我遇到过这种情况,几乎所有业务逻辑都位于规则管理系统中,有时规则甚至会从 Excel 文件生成!而且,由于项目本质上是一个 ETL 批处理,规则不应该经常更改。这一切背后的 Java 项目仅仅包含批处理框架的技术细节以及从源系统和目标系统的原始读写操作,完全没有涉及领域。

结果,所有规则都用一种团队中没人真正精通的特定语言编写,编写起来非常困难(我们的IDE无法处理这种语言),而且几乎不可能调试或测试。当需要添加新规则或更改现有规则时,团队中的大多数开发人员只是复制/粘贴现有​​规则,导致除了一个特定的更改(通常是应用该规则的字段)之外,其余文件完全相同。

如果这看起来已经很麻烦了,那么每条规则都完全没有关于其用途的线索。规则被命名为 Rule1、Rule2,多达 100 多条!而且每条规则基本上都是对硬编码值进行检查和赋值,没有任何业务术语。甚至连项目名称都无法解释整个 ETL 系统的目的。

结论

正如鲍勃大叔在他的著作《整洁架构》(Clean Architecture)中所解释的那样,在思考项目架构时,有些决策必须推迟到真正需要的时候再做,除非我们无法继续为产品增值(例如选择数据库)。其他决策则必须尽早做出,不要等到情况变得糟糕。幸运的是,这类关键决策很容易被发现,因为它们就是所谓的“架构异味”:仔细想想,它们只能是糟糕的想法,会在某个时刻回来困扰你。不幸的是,在处理遗留软件时,这类负担通常深埋在代码中,消除它们的成本非常高昂。

我们不应该害怕。没错,清理数年甚至数十年积累的烂摊子并非易事,但作为软件专业人士,我们不能任由它继续腐烂,扼杀开发人员的积极性,也绝不能让用户对我们产品的信任,以及我们为他们提供商业价值的能力。

当然,我所描述的每个架构负担都可以通过多种方式解决,因此没有灵丹妙药可以解决所有问题。但我相信每个团队都能提出最终摆脱负担的建议。所以,让我们一起面对问题,开始清理这个烂摊子吧!

鏂囩珷鏉ユ簮锛�https://dev.to/schreiber_chris/architecture-as-a-burden-18ca
PREV
一步步构建 Firefox 扩展
NEXT
React hooks:获取当前状态,返回未来