使用自动重写代码来减少技术债务





5.00/5 (1投票)
在本文中,我们将深入探讨技术债务,并为开发人员提供一些摆脱困境的解决方案。
有时,开发人员会觉得管理者希望立即完成所有工作。他们面临一个两难的境地:是花时间正确地完成工作,超出预算和截止日期,还是快速、按时、按预算完成工作,即使它并不完美。
这个选择有一个隐藏的成本:技术债务。一个快速简单的解决方案往往会引发隐藏的长期成本,因为开发人员和用户会发现需要修复的错误。一个长期的解决方案往往会过时,使维护变得困难。无论哪种方式,今天的解决方案都会累积技术债务,为明天带来更多的工作。
在本文中,我们将深入探讨技术债务,并为开发人员提供一些摆脱困境的解决方案。
技术债务的起源
在深入技术细节之前,让我们回顾一下什么是技术债务,它的起源是什么,以及它如何影响开发过程。为了简单起见,我们将考虑两种软件项目开发场景,每种场景都会以不同的方式导致技术债务。
在第一种场景中,一支仅为该项目组建的开发团队,在不断增加的需求和紧迫的截止日期下,按时完成了项目。当项目临近结束时,开发人员别无选择,只能削减开支以求生存,这时技术债务就会出现并增长。
在紧迫的条件下,可能没有足够的时间来保证代码质量。开发人员可能会通过更频繁地复制和粘贴代码片段来更快地完成功能,无论是手动还是使用代码生成器。
项目上线了,但是大量的代码重复掩盖了程序逻辑并隐藏了依赖关系,导致开发人员无法高效地进行更改。最终,在添加新功能后,开发人员花费越来越多的时间进行调试。在项目成功经历一系列阶段后,管理者会惊讶于开发团队无法按时完成后续里程碑。
每个人都在互相指责,但真正的问题是代码质量已下降到低于有效项目可维护性的临界点。补偿低代码质量不断增长的成本,就像支付不断增长的“技术债务”的利息一样。
一个由敬业开发人员组成的小团队致力于第二个项目。该项目运行多年甚至几十年。在相对轻松的环境中工作,开发人员会花时间正确地设计代码,以获得最佳的结构和可维护性。系统设计满足或超过了多年前的标准,调试通常没有问题,因为开发人员对代码了如指掌。
然而,在技术上,项目已经停滞不前。它只接受了几年中的一些小的技术升级,但主要升级通常需要完全重写。技术债务以不同的角度发展:弥补旧技术的成本不断增加,例如寻找和培训熟练的员工,获得供应商支持,以及最终由于技术限制而无法实现新需求。
这两种场景都表现出不断增加的“技术”支出,这些支出并不直接增加任何价值,只是服务于不断增长的债务。这种虚拟债务被称为“技术债务”。
在第一种场景中实施更好的员工保留、外包或给开发人员更多思考时间,以及在第二种场景中频繁地为团队增加新的技术人员,是否会有所帮助?管理专业人士正在辩论和分析这些举措的利弊。
是否有适用于这两个项目的“灵丹妙药”?每种场景都是独一无二的,没有一种解决方案适用于所有情况。你需要问这个问题——尽管存在技术问题,但是否有实质性的价值?如果你启动一个替代项目,开发和会议花费的小时数是否会异常高?逻辑的某些部分是否没有文档记录?许多人的生产力是否严重依赖于有问题的代码?
如果答案是肯定的,在尝试其他选项之前,值得考虑代码转换。精心规划的代码转换有潜力减少技术债务。更广为人知的名称是“重构”或“重写”,对于少量代码,你可以手动完成。对于大量代码行,手动转换的成本将与替代项目的成本相当或超过其成本。
是否有适用于大型项目的解决方案?自动化代码转换并不广为人知,尽管它已经存在了几十年。自动化代码转换主要为计算机语言研究人员所熟知,它确实是计算机科学核心中的一颗宝石。发现这颗宝石是我们本文的重点。
编译器理论基础
编译和自动化代码转换是同一过程的不同名称。计算机语言编译,一个程序员熟知的过程,实际上是将一种计算机语言转换为另一种语言。最初设计用于两种语言之间的直接转换,如汇编到机器码,现代编译技术越来越多地使用第三种“中间”语言。广为人知的例子包括 C# 到 Microsoft Intermediate Language (MSIL) 到机器码,以及 TypeScript 到 JavaScript 到机器码。
编译器理论提供了设计任何计算机语言转换的构建块,包括将“复制粘贴”辅助创建的代码转换为可维护的逻辑,从而重用代码。另一个例子是将“旧技术”代码重写为现代语言和平台,同时遵守当今架构所需的最佳编码实践。
这听起来难以置信,但这些任务之前已经被解决了。从编译器理论的角度来看,这些只不过是“编译器优化”,因为后者构成了以更有效、更优化的方式重写代码元素,同时保留其行为。
为了设计和实现一个专门的编译器来对负债项目执行这些“优化”,我们需要掌握编译器理论的基础。转换项目的任务可能会变得异常复杂。
编译器本质上是一个解析器。虽然你可以用许多方式开发解析器,但并非所有方式都能产生一个可维护的编译器解决方案,并且能够不断地支持新的转换。换句话说,如果你从一开始就不遵循某些规则,缺失的基础将限制建筑的高度。
因此,我们将从 Edsger Dijkstra 在创建 Algol 的第一个编译器时奠定的科学基础开始。Algol 是第一种不容易解析的语言。当 Dijkstra 创建 Algol 编译器时,它为计算机语言的演进铺平了道路,这种演进不受解析器限制,从而基于相同的原理开发了 Pascal、Basic、Fortran 及其各自的编译器。如今,Dijkstra 的方法是计算机语言编译器的标准,并已融入计算机辅助人类语音识别。它适用于任何类型的转换任务。
简单的想法是好的想法,编译器理论也不例外。主要思想是将复杂任务分解为更简单的任务,这通过将编译分为两个阶段来实现——解析和代码生成——每个阶段由多个步骤组成。
解析阶段的第一步将字符流转换为标记流。第二步将标记流转换为对象树。可选的丰富或“数据挖掘”第三步收集编码统计数据和模式,以进行性能优化和提高目标代码的可维护性。本文通过人工智能(AI)的形式,以一个微小的“专家系统”来说明事实发现。
在第二阶段,即代码生成阶段,一个经过优化和丰富后的对象模型要不被直接用于代码发射,要不被转换为另一个对象树,该对象树适合现有的目标语言代码生成器。
重写和 .NET 编译器平台 (Roslyn) 简介
代码重写或转换是指使用更好的技术替换某些语法元素,用改进的元素替代。这可以从本文所说明的局部重写,到完整的重写,即旧技术源代码被转换为现代技术源代码。在这两种情况下,都必须确定转换的源语法和目标语法。换句话说,什么必须转换为什么。
对于技术重写,您必须仔细选择目标架构和平台,或者有时通过扩展一个现代平台来创建它们,以同等地、紧凑地支持源平台中可用的功能。您通常还必须创建一个完整的源平台语法解析器和对象建模器。源对象覆盖可能需要扩展到源代码之外,并覆盖额外的元数据,如存储和用户界面。
对于局部重写,根据源语言,您可能不需要创建编译器。例如,在转换 C# 时,重写器可以建立在现有的 .NET 编译器平台之上。
为此,请打开 Visual Studio 2019 安装程序,安装“Visual Studio 扩展开发”配置文件,并选择可选功能“.NET Compiler Platform SDK”。
C# 语法解析器将源代码的字符转换为标记和对象,并返回一个“语法树”。您可以在键入语法时,在 Visual Studio 窗口中显示对象化的源代码。
如果光标放在代码中后,“语法可视化工具”窗口为空,请修改源代码中的任何字符,然后撤销修改以触发刷新。树中的每个节点都代表语法中的一个物理或逻辑元素。
.NET 编译器平台(昵称 Roslyn)还提供了一个语法生成器。只需调用树中节点的“.ToString()
”方法,即可以 C# 形式返回该节点。能够在任意字符串和语法节点之间进行双向转换,简化了重写,因为新语法通常包含旧语法的元素。
实际的 C# 代码重写
要开始,请从 GitHub 克隆源代码仓库。
在这个重写示例中,原始代码包含半重复的逻辑,通过复制和粘贴创建,并进行了原地修改以适应需求。程序员的意图是从数据库检索列表,并对列表应用有限的、多种后处理操作,然后才能在代码中使用该列表。
性能指标表明这些操作消耗了大量时间。然而,由于操作集在不同代码位置之间有所不同,因此很难缓存结果。第一个位置应用操作一和操作二,而第二个位置应用操作二和操作三。我们假设操作顺序无关紧要。
public static void ShowExample()
{
var list = GetListFromSomewhere("HELLO");
Operation1(list);
Operation2(list);
// process list
// ...
var list2 = GetListFromSomewhere("BONJOUR" + ObtainSuffix("PARIS")):
Operation2(list);
Operation3(list);
// process list
// ...
}
我们可以改进代码的几个方面。如果将各种所需的操作作为参数传递给从数据库检索列表的例程,则可以通过缓存后处理结果来提高性能。通过从过程式编程转向声明式编程来提高代码的可维护性。后者是通过用枚举的处理选项替换过程调用逻辑来实现的。
第一步是将表示原始源代码的字符串转换为对象树。
public static void Main(string[] args)
{
var tree = CSharpSyntaxTree.ParseText(@"
public class Sample
[
public void ShowExample()
{
{
var list = GetListFromSomeWhere(""HELLO"");
Operation1(list);
Operation2(list);
// process list
// ...
接下来,为了实现优化,重写器可能需要访问前面和后面的节点的信息,或者从树中收集到的其他信息。因为重写器按顺序输入节点,所以它通过收集意图的事实来提前执行代码分析。简单来说,意图的事实就是程序员真正想做的事情。它是代码背后的“更高真理”,用于可靠地将原始代码替换为更好的等效代码。
收集意图的事实得益于统计方法和人工智能。在本例中,我们使用了一个微小的“专家系统”,这是 AI 的早期形式,由 Language Integrated Query (LINQ) 组成,然后扫描语法树中的周围节点。
// analyze tree and detect facts of intention
var orinalTreeRoot = tree.GetRoot();
var detectedFacts = CodeAnalyzer.GatherFacts(originalTreeRoot);
CodeAnalyzer
类通过使用 LINQ 搜索语法树来检测可转换节点。它专注于调用“GetListFromSomewhere
”,并检查后面的同级节点以识别应用了哪些操作。输出是事实列表,例如“节点 XYZ 使用给定关键字检索了列表并应用了如此这般的各种操作”。请注意,GetListFromSomewhere
中的“关键字”不一定是字符串,而是任何返回字符串的语法表达式。您可以通过打开“快速监视”并复制粘贴表达式来获得下面的 GatherFacts
语法,而无需牢记 Roslyn 对象模型。
class CodeAnalyzer
{
public static List<GenericFact> GatherFacts(SyntaxNode syntaxRoot)
{
var keyNodes = syntaxRoot.DescendantNodes().OfType<LocalDeclarationStatementSyntax>()
.Where(m => ((IdentifierNameSyntax)
((InvocationExpressionSyntax)m.Declaration.Variables[0].Initializer.Value)
.Expression).Identifier.Value.ToString() == "GetListFromSomewhere").ToArray();
var detectedFacts = new List<GenericFact>();
一旦检测到事实,它们就会被用来填充节点替换字典。该字典包含一个新重写节点,用于在原始树中识别为“事实”的旧节点组。
// prepare node rewrites and removals
Dictionary<SyntaxNode, SyntaxNode> nodeRewrites = new Dictionary<SyntaxNode, SyntaxNode>();
foreach (var intention in detectedFacts)
{
// replace key node with rewritten node
nodeRewrites.Add(intention.KeyNode, intention.GetRewrittenNode());
foreach (var nodeToRemove in intention.OtherNodes)
{
// remove other nodes linked to fact of intention
nodeRewrites.Add(nodeToRemove, null);
}
}
通过调用转换后的树的根节点上的 ToFullString()
方法来生成最终代码。Roslyn 语法树是不可变的:它们不能被直接修改。一个特殊的 方法用于重写,它包括实现一个继承自 CSharpSyntaxRewriter
的类(SimpleNodeRewriter
)。语法树节点重写器会访问树中的所有节点。当位于一个节点上时,它可以选择不修改节点,或者创建并返回一个替换节点。
var rewriter = new SimpleRewriter(nodeRewrites);
var transformedTreeRoot = rewriter.Visit(orinalTreeRoot);
Console.WriteLine(transformedTreeRoot.ToFullString());
控制台显示重写的源代码。
后续步骤
我们已经检查了技术债务的原因及其危害,并探讨了一些解决方案,包括基于编译器理论重写代码。通过这些方法,您可以修改自己的代码,帮助减少技术债务,从而专注于未来的产品增强。
要了解有关减少技术债务的更多信息,请查看以下资源:
- Shunting-Yard Algorithm on Wikipedia (维基百科上的SHUNT车算法)
- Refactoring is the cure for Technical Debt — but only if you take it (重构是技术债务的解药——但前提是你必须采取行动)
- Morpheme Matching Based Text Tokenization for a Scarce Resourced Language (基于词素匹配的文本分词用于稀有资源语言)
- Compiler Design - Lexical Analysis on TutorialsPoint (编译器设计 - TutorialsPoint 上的词法分析)
- Expert System on Wikipedia (维基百科上的专家系统)
本文最初作为客座投稿出现在ContentLab。