遗留代码,指的是那些不再值得维护或支持的代码。在软件开发过程中,经常面临一个挑战:如何避免编写那些将来会成为遗留代码的代码。本文将探讨这个问题,并提供一些实用的技巧和原则,帮助改变这种状况。
遗留代码通常具有以下特征:设计和架构不佳,或者依赖于过时的框架或第三方组件。以下是一些典型的例子:
和团队开发了一个功能丰富的Windows应用程序。后来,意识到真正需要的是一个浏览器或移动应用程序。这时,意识到要为应用程序提供一个替代的用户界面是多么困难,因为已经将太多的领域功能嵌入到用户界面本身中。
另一个场景可能是,创建了一个后端,它深深渗透到特定的ORM(如NHibernate或Entity Framework)中,或者高度依赖于某个RDBMS。在某个时刻,想改变后端策略,避免使用ORM,使用基于文件的持久性,但然后意识到这几乎是不可能实现的,因为领域功能和数据层紧密耦合。
在上述两种场景中,都在以打字的速度产生遗留代码。
在下面的内容中,将描述一个标准企业软件开发者在典型架构演进中的三个阶段。几乎所有的开发者都会达到第二阶段,但关键是要完全通过第三阶段,这将最终使成为一个架构忍者。
大多数开发者都听说过分层架构,所以第一次尝试架构通常会看起来像这样 - 两层,前端和后端功能的责任分离:
到目前为止,看起来还不错,但很快就会意识到,应用程序的领域逻辑纠缠在平台依赖的前端和后端中,这是一个巨大的问题。
因此,下一次尝试是引入一个中间层 - 一个领域层 - 包含应用程序的真正业务功能:
这种架构看起来结构良好且解耦。然而,它并不是。问题是红色依赖箭头指示领域层对后端有硬编码依赖,通常是因为在领域层使用new关键字(C#或Java)创建后端类的实例。领域层和后端紧密耦合。这有许多缺点:
领域层功能不能在另一个上下文中单独重用。不得不拖拽它的依赖,后端。
领域层不能单独进行单元测试。不得不涉及依赖,后端。
一个后端实现(例如,使用RDBMS进行持久性)不能轻易地被另一个实现(例如,使用文件持久性)替换。
所有这些缺点都大大减少了领域层的潜在寿命。这就是为什么以打字的速度产生遗留代码。
需要做的其实很简单。必须扭转那个红色依赖箭头的方向。这是一个微妙的区别,但这是一个至关重要的区别:
这种架构遵循依赖反转原则(DIP) - 面向对象设计最重要的原则之一。关键是一旦这种架构建立起来 - 一旦那个依赖箭头的方向扭转过来 - 领域层的潜在寿命就大大延长了。UI需求和趋势可能会从Windows切换到浏览器或移动设备,首选的持久性机制可能会从基于RDBMS的切换到基于文件的,但现在这一切都相对容易交换,而不需要修改领域层。因为此时,前端和后端都与领域层解耦了。因此,领域层变成了一个代码库,理论上永远不需要替换 - 至少只要业务领域和整体编程框架保持不变。现在,正在有效地对抗遗留代码。
让给一个简单的例子,说明如何在实践中实现DIP:
也许有一个产品服务在领域层,可以在后端定义的存储库上执行CRUD操作。这通常会引导出一个依赖图,依赖箭头指向错误的方向:
这是因为在产品服务中的某个地方,会"new"一个依赖到产品存储库:
C# var repository = new ProductRepository();
要使用DIP扭转依赖的方向,必须在领域层引入一个产品存储库的抽象形式,即IProductRepository接口,并让产品存储库成为这个接口的实现:
C# private readonly IProductRepository _repository; public ProductService(IProductRepository repository) { _repository = repository; }
这就是所谓的依赖注入(DI)。之前在一篇名为"Think Business First"的博客文章中更详细地解释了这一点。
一旦建立了正确的整体架构,对抗遗留代码的目标就显而易见了:尽可能多地将功能移动到领域层。让那些前端和后端层缩小,让那个领域层变得丰满:
这种架构的一个非常方便的副产品是,它使得建立领域功能的单元测试变得容易。由于领域层的解耦性质,以及它的所有依赖都由抽象(如接口或抽象基类)表示,所以很容易建立这些抽象的伪造对象,并在建立单元测试固定装置时使用它们。所以为整个领域层提供单元测试是"轻而易举"的。应该争取100%的单元测试覆盖率 - 使领域层非常健壮和坚固,这将再次增加领域层的寿命。
作为一个企业软件开发者,避免以打字的速度产生遗留代码是一个持续的战斗。要取得胜利,请执行以下操作:
确保所有这些依赖箭头指向中心和独立的领域层,通过应用依赖反转原则(DIP)和依赖注入(DI)。
不断滋养领域层,尽可能多地将功能移动到其中。让那个领域层变得丰满和沉重,同时缩小外层。
用单元测试覆盖领域层的每一个功能。
遵循这些简单的规则,一切都会水到渠成。编写的代码将可能比以前有更长的寿命,因为:
领域层功能可以在许多不同的上下文中重用。
领域层可以通过100%的单元测试覆盖率变得非常健壮和坚固。