从 Windows Forms 到微服务的经验教训

                                      Photo by Dan Counsell on Unsplash

如果说软件开发中有什么不变的东西,那就是变化。

在 .NET 生态系统中摸爬滚打的这二十年里,我见证了各种框架的起起落落,目睹了容器化的崛起,也曾为微服务架构摇旗呐喊------而在几年前,微服务对许多人来说还只是晦涩难懂的概念。

然而,如果说这个领域还有另一件永恒的事,那就是技术债务------它始终存在,而且如果不加以管理,就会从一个微不足道的小麻烦演变成巨大的绊脚石。

幸运的是(或者不幸,取决于你怎么看),我几乎经历过所有形式的技术债务。我最早接触的是 Windows Forms 项目,当时这可是最前沿的技术。后来,我开始构建 ASP.NET MVC 的单体应用,接着又挑战过连接到庞大单体数据库的嵌入式软件,之后将旧的单体架构迁移到微服务,最终踏上了可扩展分布式系统的架构之路。

在每个阶段,技术债务的表现形式都在变化,但其根本原因和解决方案却惊人地一致:短视的决策、忽视最佳实践、赶工期,以及缺乏清晰的沟通------这些因素共同推动了一个恶性循环,如果不加遏制,即便是最优秀的团队也会被拖垮。

在这里,我想分享自己这些年来积累的个人见解:技术债务是如何产生的,如何尽早识别它,以及如何在不影响新功能开发进度的前提下加以应对。

早期岁月

刚开始接触 .NET 时,我在做 Windows Forms 应用开发。

那时候很激动,因为我们正在从 VB5、VB6 和 Visual J++ 这些老旧的客户端-服务器技术,过渡到(在当时看来)更现代化的 C# 环境。Windows Forms 带来的 UI 可能性让我们兴奋不已------拖拽控件、窗体事件、集成数据库连接,相比于老框架而言,这一切都让人觉得是一次革命。

但这些项目也让我第一次接触到了技术债务的问题。那时候,我们很容易把所有东西都写在同一个项目文件里,或者只用少量几个解决方案。

每个窗体都带着超大的代码隐藏文件,逻辑和 UI 交织在一起,改动一个地方就可能弄崩六七个功能。到处可见所谓的"上帝类"(God class),它们从数据访问到业务逻辑再到 UI 渲染,什么都管。

我们甚至会直接在按钮点击事件里写数据访问代码。随着时间推移,这些代码隐藏文件变得越来越庞大,调试它们简直就是一场噩梦。

我们是走一步算一步地写代码,只关注当下能跑起来,而不是为未来的扩展做好规划。

起初,这种技术债务是隐形的,毕竟所有东西都集中在一起。但一旦应用需要扩展,我们就得重写大量代码才能适应新的需求。这是我第一次真正意识到,在软件开发中短视思维会带来怎样的后果。

                                      Photo by Glenn Carstens-Peters on Unsplash

Web 开发的崛起

最终,世界大规模转向了 Web,.NET 开发者也随之跟进。ASP.NET Web Forms,后来又有了 ASP.NET MVC,成为了新的前沿技术。

有了 MVC,我们终于有了一个结构化的方法,可以更好地组织代码。但那些从 Windows Forms 过渡过来的开发者很快发现,仅仅遵循 MVC 模式并不能保证代码的整洁。

我们仍然需要严格执行这些分离。

尽管 MVC 结构天然地引导开发者实现更好的关注点分离,但当需要添加重大新功能或适应新的业务需求时,大型单体应用依然会遇到问题。

在压力之下,人们往往会忍不住把控制器和业务逻辑混在一起,让数据访问层变得混乱不堪。一旦应用需要集成一个新系统------比如第三方 API 或新的支付网关------我们就会发现,现有的数据模型和控制器根本不够灵活。

结果呢?仓促的补丁,让本就不堪重负的代码库变得更加复杂。

那个时候,我意识到了一些至关重要的道理。

首先,采用像 MVC 或者 MVVM 这样的成熟模式,并不能自动消除技术债务。

其次,真正的关键在于持之以恒的纪律------定期回顾代码,重构、优化,保持模块化。

第三,如果不腾出时间去重构和维护代码库,你积累的技术债务会很快超过你赶工开发所节省的时间。

转向嵌入式系统和大型数据库

在我的职业旅途中,有一段时间我开始接触嵌入式系统,而这些系统仍然依赖于庞大的单体数据库。这种环境带来了独特的挑战,尤其是在性能和资源限制方面。

Web 应用可以把部分任务卸载出去,或者通过增加服务器实现横向扩展,但嵌入式系统的硬件资源有限,因此必须精打细算地进行优化。

在这些项目中,技术债务往往表现为性能债务。为了赶工期,我们会在编写高效代码时偷懒,结果就是,等到设备在真实场景下运行时,整个系统慢得像乌龟爬。

另一个大问题是,整个应用(包括业务逻辑,甚至 UI 逻辑)都和数据库模式紧紧耦合在一起。改动一个表结构,就意味着要重写大量代码。数据迁移和版本管理变成了一场噩梦,而一个小小的失误就可能让整个系统彻底崩溃。

我对这些经历记忆犹新,它让我深刻体会到了在数据层保持模块化设计的重要性。

即使你没有在构建微服务架构,把数据库模式和应用逻辑解耦,依然能带来极大的好处。

使用存储过程、合理的数据访问层,以及精心设计的数据契约,可以帮助你避免层层连锁的崩溃。简单来说,关注点分离的原则,在数据层和应用层同样重要。

                                              Photo by cottonbro studio

从单体架构迁移到微服务

当微服务开始流行时,我开始参与一些项目,目标是把大型单体应用拆分成更小、更容易独立部署的服务。

这是我职业生涯中最具启发性的经历之一。微服务架构承诺解决很多问题------可扩展性、可维护性,以及更快的发布周期。但它同时也带来了新的复杂性,从分布式系统管理,到服务间通信和数据一致性问题。

事实证明,在微服务环境中积累技术债务,跟在单体架构里一样容易。事实上,如果没有强大的 DevOps 实践、自动化测试,以及清晰的服务边界,风险甚至更高。

此外,我见过一些团队只是"表面上"在做微服务,每个服务仍然严重依赖共享数据库或一堆共享的数据模型库。这种"半解耦"很快就会导致一张纠缠不清的依赖网,讽刺的是,这样的系统可能比原来的单体架构还难维护。

尽管存在潜在的陷阱,但如果做得正确,微服务架构确实能带来真正的优势。把应用拆分成更小、更清晰定义的服务,可以减少变更和故障的影响范围。如果某个服务崩了,它可能不会拖垮整个系统。

从技术债务的角度来看,你可以单独重构或重新设计某个服务,而不会影响整个应用。关键在于严谨的规划------明确边界,确保服务是真正独立的,并且在监控、日志记录和 CI/CD 基础设施上做好投资。

扩展架构与云原生方法

近年来,我一直在深入参与基于 Azure 和 AWS 构建可扩展的云原生解决方案(尽管我个人更偏向 Azure,因为它与 .NET 的集成更流畅)。

如今的解决方案通常涉及 Docker 容器化,以及 Kubernetes 编排,或者按需启动的无服务器函数。这种环境提供了前所未有的灵活性,但同时也要求有严格的架构纪律,以防技术债务悄然积累。

在基础设施层面控制技术债务,最大的帮手之一就是 IaC(基础设施即代码)工具,比如 Terraform 或 Azure Resource Manager (ARM) 模板。显然,通过像管理代码版本一样管理基础设施版本,你可以跟踪变更,必要时轻松回滚,并确保开发、测试和生产环境的一致性。

微服务和云原生架构的复杂性意味着你需要高级的监控能力。像 Azure Insights、ELK 和 Grafana 这样的工具,能让你在问题变成紧急情况之前先发现它们。这种前置性的洞察可以大大降低大规模技术债务的可能性,因为你能更早发现性能瓶颈或架构上的反模式。

此外,记住云原生解决方案是自然演进的。

当你启动一个新容器,或者改变函数应用的触发方式时,你其实是在迭代架构。只要有意为之------监控使用情况、衡量性能、收集反馈并相应调整,你就能控制住技术债务。

及早识别技术债务

我在职业生涯中学到的最宝贵的经验之一就是:及早发现技术债务,等于赢了一半。 早一点修复 bug 或重构代码,成本永远比晚了再处理要低。但在紧迫的截止日期和不断增加的功能需求下,怎么才能察觉到技术债务正在悄悄堆积?

• 代码异味(Code Smells):这些都是明显的红旗,比如超长的方法、重复的逻辑、魔法字符串,或者那些无所不包的类。

• 新成员上手缓慢:如果新开发者需要很长时间才能理解代码库,或者经常在复杂的依赖关系中迷失,那基本可以确定系统存在架构问题。

• 回归 bug 过多:如果每次新增功能都会无缘无故弄崩其他地方,那很可能是隐藏的耦合问题,或者代码本身过于脆弱。

• 总是在救火:如果你们的日常工作一直是在修复线上问题,而不是主动优化系统,那就是技术债务积累过多的警告信号。

留意这些迹象。它们是你的早期预警系统,提醒你需要腾出时间来重构、优化测试,或者甚至重新设计某些组件。

                                                      Photo by Tara Winstead

应对技术债务的策略

管理技术债务,既是技术问题,也是思维方式的问题。我想分享一些对我来说行之有效的核心策略。

我发现一个有效的做法是,在每个 Sprint 或发布周期里专门留出时间处理技术债务任务。这样可以确保你在交付新功能的同时,也能逐步削减旧有问题。而自动化测试能让你放心地进行重构,而不会破坏已有功能。

再配合持续集成(CI)流水线,每次提交代码时都会自动触发测试,这样可以尽早发现问题。

另外,记住要践行"童子军原则"(Boy Scout Rule)!这是从 Robert C. Martin 那里借来的一个原则,它的意思是:每次修改代码时,都应该让代码库比你接手时更干净一点。即使是微小的改进,长期积累下来也会产生巨大的变化。

同时,使用 Architecture Decision Record (ADR) 或者 Wiki 页面,记录下为什么做了某些架构决策。这些背景信息可以防止未来的团队重蹈覆辙。

最后一个建议是:向非技术人员传达忽视技术债务的代价。他们不需要了解所有细节,但应该明白,如果不解决技术债务,未来的成本会更高,交付速度也会变慢。

真实案例

让我分享几个(已经脱敏处理的)故事,展示技术债务如何在不同的管理方式下,决定一个项目的成败。

有一次,我参与了一个大型 ASP.NET MVC 应用的重构,这个应用经过多年开发,变得越来越难以维护。我们决定来一次彻底大重构,把它拆成微服务。

但我们没有明确的服务边界,没有合理的 DevOps 策略,也没有系统化的测试方案。

结果?项目被拖延了一整年,成本超支,团队士气低落。最终,我们不得不推倒重来,回到单体架构,然后以业务能力为指导,逐步拆分成一个个独立的微服务。

另一边,在另一个项目里,我们面对的是一个每天都在运行的关键业务系统。我们没有一上来就推倒重写,而是先识别出最棘手的模块,并优先把它们拆分成独立服务。我们为这些新服务搭建了 CI/CD 流水线,并逐步引入现代架构设计。

在一年时间里,我们成功将整个系统的 50% 迁移到微服务架构,而没有影响日常业务运行。每次小步前进,都给团队带来了信心,也让企业感到安心。

这两个案例形成了鲜明对比,揭示了一个核心道理:应对技术债务,需要有明确的计划,并且要让所有人理解你的策略。

一刀切的大重构听起来很有吸引力,但往往会因其复杂性而失败。

最终思考

从 Windows Forms 应用,到 MVC 单体架构,再到带着庞大数据库的嵌入式系统,最终到云上的现代微服务架构,我深刻认识到:技术债务是软件开发生命周期中无法避免的一部分。

问题不是你会不会积累技术债务,而是你能多快意识到它的存在,以及你是否有足够的纪律去应对它。

从我的经验来看,.NET 生态系统提供了丰富的工具和框架,来帮助我们提高代码质量和架构规范。从高层次的设计模式,到自动化测试框架,我们拥有足够的手段来将技术债务控制在可接受的范围内。

相关推荐
果冻人工智能3 天前
让我们从零开始使用PyTorch构建一个轻量级的词嵌入模型
#人工智能·#ai代理·#ai应用·#ai员工·#神经网络·#ai
果冻人工智能4 天前
在 PyTorch 中理解词向量,将单词转换为有用的向量表示
#人工智能·#ai代理·#ai应用·#ai员工·#cnn·#chatgpt·#神经网络·#ai
果冻人工智能6 天前
跟着蚂蚁走,它们知道路:用 ACO-ToT 增强 LLM 推理能力
#人工智能·#ai代理·#ai应用·#ai员工·#神经网络·#ai
果冻人工智能10 天前
基于生成式AI的访问控制, 迁移传统安全策略到基于LLM的风险分类器
#人工智能·#ai代理·#ai应用·#ai员工·#cnn·#神经网络·#ai
果冻人工智能1 个月前
人类讨厌AI的缺点,其实自己也有,是时候反思了。
#人工智能·#ai代理·#ai应用·#ai员工·#cnn·#神经网络·#ai
果冻人工智能1 个月前
您的公司需要小型语言模型
#人工智能·#ai代理·#ai应用·#ai员工·#cnn·#神经网络·#ai
果冻人工智能2 个月前
主动式AI(代理式)与生成式AI的关键差异与影响
#人工智能·#ai代理·#ai应用·#ai员工·#cnn·#chatgpt·#神经网络·#ai
果冻人工智能2 个月前
创建用于预测序列的人工智能模型,用Keras Tuner探索模型的超参数。
#人工智能·#ai代理·#ai应用·#ai员工·#cnn·#chatgpt·#神经网络·#ai
果冻人工智能2 个月前
创建用于预测序列的人工智能模型,调整模型的超参数。
#人工智能·#ai代理·#ai应用·#ai员工·#cnn·#chatgpt·#神经网络·#ai