English
Technical debt is the accumulated cost of shortcuts taken in software development that must eventually be paid. When a team chooses a quick solution over a correct one — using a global variable instead of designing proper state management, copying code rather than extracting it into a reusable module, deploying without automated tests because the deadline is tomorrow — the work gets done faster, but future work becomes slower. The metaphor, proposed by Ward Cunningham in 1992, comes from financial debt: borrowed money lets you do something now that you could not otherwise afford, at the cost of interest payments that reduce your capacity for future investments. Technical debt is similar: borrowed simplicity lets you ship now, at the cost of reduced development speed later.
The debt metaphor is useful and incomplete. Financial debt is quantifiable; technical debt is not. You can measure the amount you owe to the bank; you cannot measure the accumulated awkwardness of a codebase that has grown without architectural attention. Financial debt accrues interest at a known rate; technical debt’s carrying cost varies by where in the codebase it lives, how often that code is touched, and how the system’s requirements are changing. But the metaphor captures something important: shortcuts in the present reduce capacity in the future, and the longer debt is carried, the more expensive it becomes to pay down. Systems that accumulate debt without ever paying it down eventually reach a state where further development is so slow that the system must be rewritten or abandoned.
Understanding technical debt — what creates it, how it compounds, how to manage it, and when it is a legitimate choice versus an emergency — is essential for any practitioner who works on software over time horizons longer than a single release. That is most practitioners. The pressures to take shortcuts are real and persistent; the tools to manage the consequences are specific and learnable; and the judgment to distinguish acceptable debt from dangerous debt requires the conceptual framework that this section provides.
Prerequisites: Architecture (§7.1) — technical debt often manifests as architectural problems. Testing (§7.2) — tests make refactoring safe. Software process (§7.3) — debt management is a process concern. Programming (§2.1) — you need to read and modify code to refactor it.
From Borrowed Simplicity to the Economics of Software Evolution
Ward Cunningham coined the term “technical debt” in 1992, in the context of a financial software system he had built at Casio. He described the situation in a talk: the team had shipped quickly by not spending time on full understanding, and the resulting system worked but needed to be refactored before further features could be added. He framed this explicitly as debt: you borrow against the future by doing things you know are not quite right, and you pay it back by refactoring.
Cunningham was using the metaphor to communicate to non-technical stakeholders — explaining why the team needed time to “clean up” the code before adding new features. The metaphor worked because financial stakeholders understood debt. It also worked because it captured something real: the relationship between speed now and speed later, mediated by code quality.
The concept spread and bifurcated. The original Cunningham usage described a specific kind of intentional debt: code that was written with less than full understanding, with the intention to refactor once understanding was achieved. This is sometimes called “deliberate, prudent” debt — the team knew it was taking a shortcut and planned to pay it back. Martin Fowler’s 2009 Technical Debt Quadrant added three other categories: reckless debt (shortcuts taken without awareness of the consequences), inadvertent debt (problems that only became apparent in retrospect, when the team had learned more), and the combination of deliberateness and recklessness. Real codebases contain all four types, often in different proportions in different parts of the system.
Martin Fowler’s Refactoring: Improving the Design of Existing Code (1999) provided the technical vocabulary for the activity of paying down debt. Refactoring is the process of restructuring existing code without changing its observable behavior — the same inputs produce the same outputs, but the internal structure is improved. Fowler catalogued over sixty named refactorings: Extract Method (taking code out of one method and into a named new one), Move Method (relocating a method to the class that uses it most), Rename (choosing a better name for a variable, method, or class), Introduce Parameter Object (replacing a cluster of related parameters with a single object). The catalog did for refactoring what the Gang of Four’s patterns had done for design: gave practitioners a shared vocabulary and a reference for common operations.
The critical companion to refactoring is automated tests. Without tests, refactoring is reckless: you restructure the code and have no way to verify that the behavior has not changed. With tests, refactoring is safe: each small transformation can be verified immediately. The test suite is what makes the “same behavior” guarantee achievable in practice. This is why test-driven development and refactoring evolved together — they are two aspects of the same discipline. The Red-Green-Refactor cycle makes refactoring the third mandatory step after making tests pass: not just “make it work,” but “make it clean.”
Michael Feathers’s Working Effectively with Legacy Code (2004) addressed the specific challenge of systems that lack the test coverage that makes safe refactoring possible. Feathers’s definition of “legacy code” — code without tests — is deliberately provocative: it makes the problem of legacy code tractable by giving it a technical definition and a solution path. The book’s strategies — characterization tests (tests written to document current behavior, not desired behavior), seam identification (finding points where behavior can be changed without modifying the code directly), and the various dependency-breaking techniques — provide the tools for incrementally adding coverage to existing systems so that refactoring becomes possible.
Manny Lehman’s laws of software evolution, developed through empirical study of IBM software in the 1970s and refined through the 1990s, provided an empirical foundation for understanding how software systems change over time. The most important laws: a software system, once released, must be continually adapted or it becomes progressively less satisfactory (the law of continuing change); as a system evolves, its complexity increases unless work is done to reduce it (the law of increasing complexity); the rate of growth of systems has a tendency to self-regulation (the law of self-regulation). Lehman’s laws describe what happens to systems that are not managed — they accumulate complexity, slow down, and become progressively less satisfactory. Managing against Lehman’s laws is the work of technical debt management.
The economic framing of technical debt received its most systematic treatment from Steve McConnell’s “Managing Technical Debt” (2007) and subsequent work by various researchers on quantifying the carrying cost of technical debt. The key insight was that technical debt has a principal (the cost to fix the problem) and interest (the additional time that must be spent on future work because the problem is not fixed). A poorly factored authentication module that takes twice as long to modify as it should costs half a developer-day in interest every time a feature touches authentication. If authentication is touched five times per month, the carrying cost is 2.5 developer-days per month — far more than the cost of refactoring the module. The economics of refactoring are favorable whenever the carrying cost exceeds the principal; systems that carry expensive debt in frequently-touched code are systematically more expensive to develop than they would be with well-factored code.
The contemporary landscape of technical debt management has been transformed by static analysis tools that can identify specific patterns associated with debt: code duplication (which SonarQube and similar tools measure), cyclomatic complexity (which correlates with testing difficulty), coupling metrics (which reveal architectural problems), and code churn rate (which correlates with the frequency with which code is modified and thus the importance of its quality). AI-assisted refactoring tools can suggest specific refactorings, identify dead code, and generate the characterization tests that make refactoring safe. These tools have made debt identification cheaper; the judgment about which debt to pay down first, and how, remains a human decision.
The Economics of Code, Refactoring as Practice, and When to Rewrite
The Cost Model of Technical Debt
Technical debt has economic structure that determines when paying it down is worthwhile and when carrying it is the right choice.
The principal of a debt item is the cost to fix it: the effort required to refactor the code, add the missing tests, or redesign the architecture. The interest is the ongoing cost the debt imposes: extra time required to work around a confusing abstraction, extra bugs introduced by duplicated code that must be kept in sync, extra cognitive load for developers who must understand a complex module before modifying it.
Interest rates vary dramatically by debt location. Debt in code that is rarely touched has low interest: if nobody modifies the authentication module for six months, its poor factoring costs nothing during those six months. Debt in frequently modified code has high interest: if the data model is modified weekly, its awkward design costs something every week. The code churn rate — how often code is modified — is the primary determinant of debt interest rate. Focusing debt paydown on frequently-modified code produces the highest return.
Debt also interacts with defect rate. Code that is poorly factored is harder to understand and therefore harder to modify correctly; poorly factored code has higher defect rates than well-factored code that implements the same behavior. The additional defect cost — the time spent finding, diagnosing, and fixing bugs — is an additional form of interest that debt accumulates. Systems with high technical debt have systematically higher defect rates, which is both an economic cost (fixing bugs is expensive) and a quality cost (defects affect users).
The quadrant that organizes debt types by deliberateness (whether the team knew they were taking a shortcut) and prudence (whether the shortcut was warranted) clarifies when debt is legitimate. Deliberate and prudent debt — “we know this is not the right design, but we ship it now to validate the market, and we will refactor when we know the product is viable” — is a valid business decision. Reckless and inadvertent debt — accumulated because the team was not paying attention, did not have the skills to know better, or was under pressure that prevented any attention to quality — is purely cost. Most real codebases contain a mixture; distinguishing between them determines what the remediation strategy should be.
Refactoring: The Practice of Controlled Change
Refactoring is structural improvement without behavioral change. The constraint — the same inputs produce the same outputs after refactoring — is what makes refactoring safe: it is not a functional change, so it should not introduce new bugs (with tests to verify the constraint). The purpose of refactoring is to improve the code’s internal structure so that future changes are easier, faster, and less likely to introduce bugs.
The discipline of refactoring requires several supporting conditions. First, tests: every refactoring should be verified by automated tests that confirm the behavior has not changed. Starting a refactoring without tests is starting without a safety net; the inevitable mistakes cannot be caught automatically. Second, small steps: each individual refactoring transformation should be small enough to be clearly safe. Large refactorings that restructure many parts of the system at once are risky because the combination of changes may interact in unexpected ways. The Extract Method refactoring, for instance, is safe because it moves well-bounded code into a new method with a clear name; the risk of changing observable behavior is low. Redesigning the data model across fifty modules simultaneously is not a refactoring; it is a rewrite.
The catalog of refactorings provides the vocabulary for thinking about structural improvement. Beyond the individual named refactorings, the higher-level refactoring goals — reduce duplication, improve naming, reduce class size, reduce method length, clarify dependencies, invert control — provide the direction for refactoring efforts. The boy scout rule — leave the code slightly cleaner than you found it — is the discipline that makes continuous refactoring practical without dedicated refactoring sprints: each time a developer works in a module, they make it slightly better, so the cumulative effect of ongoing development is improvement rather than degradation.
Opportunistic refactoring — refactoring code while developing a feature that touches that code — is more sustainable than scheduled refactoring. A dedicated “refactoring sprint” takes the team off feature delivery for a sprint, creates resistance from stakeholders who see feature delivery slow, and addresses technical debt at a rate that is unlikely to outpace its accumulation. Opportunistic refactoring addresses debt in the code being touched and costs are invisible in feature delivery time: the feature delivery time includes the refactoring time, but the refactoring reduces the time for subsequent features in the same area.
The Rewrite Question
The accumulation of technical debt eventually raises the question of whether to continue with incremental refactoring or to rewrite from scratch. Joel Spolsky’s 2000 essay “Things You Should Never Do” argues strongly against rewrites: the existing code, however ugly, encodes decades of bug fixes and edge case handling that are not visible in the code but would be rediscovered painfully in a rewrite. Rewrites are rarely delivered on time, often fail to replicate the full behavior of the original, and leave the organization without the ability to continue developing the original while the rewrite is in progress.
The argument for rewrites is that some codebases are so deeply structured around incorrect assumptions that incremental refactoring cannot reach the problems — it would be cheaper and faster to redesign from scratch with the benefit of the knowledge accumulated from the original system. This argument is sometimes correct; there are systems where the architecture is so fundamentally wrong that no amount of incremental improvement will make them maintainable.
The strangler fig pattern (Martin Fowler, 2004) provides the synthesis: gradually replace a legacy system by building a new system alongside it, routing specific functionality to the new system as it is built, until the original system is entirely replaced. This approach provides the benefits of a redesign — cleaner architecture, better understanding — while maintaining operational continuity. The old system continues to run and handle the functionality that has not yet been migrated; the new system grows alongside it. The metaphor is a strangler fig tree that grows around a host tree, gradually replacing it as the host dies.
The technical and economic analysis of the rewrite question should be specific: what is the ongoing carrying cost of the current system? what would a rewrite take? what is the risk that the rewrite fails or misses the deadline? The decision should not be driven by developer aesthetics — “we want to use the new framework” — but by the economics of the current system’s carrying cost versus the cost and risk of replacement.
What Studying This Changes
Technical debt and refactoring changes how practitioners think about code as a long-lived asset rather than a short-lived artifact.
The first change is the ability to make explicit the cost of shortcuts. When a team is under pressure to take a shortcut, the practitioner who understands technical debt can say: “this shortcut will add approximately X developer-days of carrying cost per month to this module. To pay it off will take Y developer-days. Given that we touch this module frequently, we should plan to pay it off within Z weeks.” This converts a vague sense that “we should clean this up eventually” into a business argument with specific costs and timelines.
The second change is the vocabulary for prioritizing refactoring. Not all technical debt is equally expensive; debt in frequently-modified code costs more in interest than debt in stable code. The practitioner who understands debt economics can prioritize refactoring based on code churn — focusing refactoring effort on the code that is most frequently modified and therefore most expensive to carry in poor condition.
The third change is confidence in making structural changes. Practitioners who have studied refactoring and who maintain good test coverage can make structural changes to existing code with confidence — not reckless confidence, but the calibrated confidence that comes from knowing that each small refactoring step is verified by tests and that the accumulation of small safe steps produces large-scale improvement without the risk of large-scale rewrite. This confidence reduces the cost of improving code quality, which makes it more likely to happen.
The fourth change is the judgment to distinguish legitimate debt from dangerous debt. Taking on deliberate, prudent debt to ship a feature that validates a product direction is a legitimate business decision. Accumulating reckless debt because the team does not have time to do things right is a red flag about the team’s working conditions. The practitioner who can make this distinction can participate productively in conversations about technical debt strategy rather than either dismissing debt concerns or catastrophizing about code quality.
Resources
Books and Texts
Fowler’s Refactoring: Improving the Design of Existing Code (2nd ed., Addison-Wesley, 2018) is the foundational text. The second edition updates the catalog to JavaScript examples while maintaining the conceptual framework of the first edition. Reading Part I (chapters 1-3, the worked example, the principles, the code smells) provides the essential conceptual framework; the catalog of refactorings in the rest of the book is reference material to consult as needed. The catalog is also available (in an older version) at refactoring.guru for online reference.
Feathers’s Working Effectively with Legacy Code (Prentice Hall, 2004) is the practical guide to improving code that lacks the test coverage that makes refactoring safe. Its dependency-breaking techniques and characterization testing approach are the specific tools for the specific problem of adding coverage to untested legacy code. No other book addresses this problem as directly or as completely.
Beck’s Test-Driven Development: By Example (§7.2 reference) provides the Red-Green-Refactor cycle that makes refactoring a continuous practice rather than a periodic cleanup activity.
McConnell’s Code Complete (2nd ed., Microsoft Press, 2004) provides the broader context for code quality decisions, covering construction practices, variable and function naming, code organization, and defensive programming. Its chapter on design is particularly relevant to technical debt: decisions made at construction time that are difficult to change later.
Richardson’s Microservices Patterns: With Examples in Java (Manning, 2018) addresses the specific form of technical debt created by monolithic architectures that need to be decomposed — the most common large-scale refactoring challenge in contemporary software.
| Book | Role | Type |
|---|---|---|
| Fowler, Refactoring (2nd ed.) | Foundational refactoring text | Depth |
| Feathers, Working Effectively with Legacy Code | Legacy code improvement | Depth |
| Refactoring-loop reference: Beck, Test-Driven Development: By Example | Red-green-refactor discipline | Depth |
| McConnell, Code Complete (2nd ed.) | Construction quality foundations | Depth |
| Richardson, Microservices Patterns | Monolith decomposition patterns | Depth |
| Tornhill, Software Design X-Rays | Behavioral code analysis and hotspot-based debt management | Depth |
Courses, Papers, and Current Sources
Cunningham’s original 1992 OOPSLA talk where the technical debt metaphor was introduced, and his subsequent clarifications, are available through the Ward Cunningham wiki (c2.com). Reading the original framing — deliberate debt taken with the intent to refactor, not debt taken carelessly — provides context that secondary accounts often miss.
Fowler’s “Technical Debt Quadrant” blog post (2009, free at martinfowler.com) is the foundational secondary source for the quadrant categorization. His catalog of refactorings at refactoring.com provides the online reference version of the book’s catalog.
Spolsky’s “Things You Should Never Do, Part I” (2000, free at joelonsoftware.com) and Fowler’s “Strangler Fig Application” (2004, free at martinfowler.com) are the primary texts for the rewrite debate.
| Resource | Platform | Type |
|---|---|---|
| Cunningham, c2.com technical debt pages (free) | c2.com | Depth |
| Fowler, “Technical Debt Quadrant” (free) | martinfowler.com | Reference |
| Fowler, “Strangler Fig Application” (free) | martinfowler.com | Reference |
| Spolsky, “Things You Should Never Do” (free) | joelonsoftware.com | Depth |
| Code Red code quality study (free) | arXiv | Reference |
Practice, Tools, and Current Sources
SonarQube (community edition free, sonarqube.org) analyzes code for specific patterns associated with technical debt: duplication, complexity, code smells, security vulnerabilities. Running SonarQube on an existing codebase and examining the debt report — which files have the most complexity, which have the most duplication — is the fastest way to see what technical debt measurement looks like in practice.
Code Climate (commercial with free tier) provides similar analysis with the additional metric of code churn: which files are modified most frequently. The combination of complexity and churn identifies the highest-priority refactoring targets — code that is both messy and frequently modified.
The refactoring.guru catalog provides visual illustrations of each refactoring pattern with before/after examples. Using it alongside Fowler’s book makes the catalog more accessible; the visual format shows the structural change more clearly than prose description alone.
Performing a refactoring in an IDE — using the IDE’s built-in refactoring tools (rename, extract method, move class) rather than manual text editing — is the most efficient way to develop refactoring fluency. The IDE’s refactoring tools apply the transformation safely (handling all the references to a renamed symbol, for instance) and verify that the result compiles; they are the practical implementation of the “small safe steps” discipline.
| Resource | Platform | Type |
|---|---|---|
| SonarQube Community (free) | sonarqube.org | Practice |
| Code Climate (free tier) | codeclimate.com | Practice |
| CodeScene (commercial; free trial/options may vary) | codescene.io | Practice |
| OpenRewrite (free; Moderne platform paid) | openrewrite.org / GitHub | Practice |
| Renovate (open source; hosted options may be paid) | docs.renovatebot.com | Practice |
| refactoring.guru (free) | refactoring.guru | Reference |
| IDE refactoring tools (built-in) | IntelliJ / VS Code / Eclipse | Practice |
Traps
| Trap | Why it misleads | Better response |
|---|---|---|
| Treating technical debt as always bad | Technical debt is sometimes a legitimate business decision. Taking on deliberate, prudent debt to ship a feature that validates a product direction is different from accumulating reckless debt from carelessness. Treating all technical debt as equally bad conflates the two, makes debt discussions unproductive (because developers feel accused of poor practices when they have made reasonable tradeoffs), and leads to over-engineering in situations where the shortcut is genuinely the right choice. | Use the debt quadrant to distinguish types of debt. For deliberate debt, be explicit about the intent to pay it off and track it. For inadvertent debt, diagnose how it accumulated to prevent recurrence. For reckless debt, address the conditions that produced it. The conversation about whether a given shortcut is prudent or reckless is a productive one; the blanket judgment that shortcuts are always wrong is not. |
| Scheduled “refactoring sprints” instead of continuous refactoring | Dedicating a sprint to refactoring (a) reduces the feature velocity that stakeholders see, creating organizational resistance; (b) addresses debt at a point in time rather than continuously, so debt accumulates between sprints; and (c) rarely keeps pace with the rate of new debt accumulation. Teams that do periodic refactoring sprints typically find that the debt grows faster than the sprints can address. | Practice the boy scout rule: leave the code slightly better than you found it, every time you touch it. Opportunistic refactoring — improving code as you work in it — is invisible in feature delivery timelines and creates no organizational resistance. The aggregate effect of consistent small improvements exceeds the effect of periodic large refactoring efforts, at lower organizational cost. |
| Refactoring without tests | Refactoring without automated tests to verify that behavior has not changed is restructuring, not refactoring. Every structural change to code carries the risk of introducing bugs; without tests, these bugs go undetected until they appear in production. Teams that “refactor” without tests regularly introduce bugs while believing they have only improved structure. | Establish test coverage before refactoring. If coverage is insufficient, use characterization tests (as described in Feathers) to document the current behavior before changing the structure. The investment in characterization tests pays off immediately in confidence that the refactoring has not changed behavior, and the tests remain valuable for future changes. |
| The second-system effect in rewrites | The team that rewrites a legacy system is rarely building system 2; they are building the first system for the second time, with all the over-engineering impulses that come from the freedom to start fresh. Rewrites typically take longer than estimated, often fail to replicate all the behavior of the original (discovering missing requirements only after the rewrite is deployed), and introduce new bugs where the original had evolved to correctness. | Before committing to a rewrite, explicitly account for the second-system effect: the tendency to add features and “do it right this time” that extends timelines. Use the strangler fig approach when possible — migrate incrementally, replacing the original piece by piece while maintaining operational continuity. If a complete rewrite is genuinely necessary, scope it tightly to replication of existing behavior and postpone improvements to after the migration is complete. |
| Debt tracking without paydown | Many teams track technical debt in issue trackers without actually addressing it. The debt backlog grows, items age, and the act of tracking provides the organizational comfort of “we know about this” without the actual reduction of debt. Debt that is tracked but never paid down costs the same as debt that is not tracked; tracking adds overhead without benefit. | For each tracked debt item, assess its carrying cost. If the carrying cost is significant (the code is frequently modified and the debt makes modification slow), prioritize paying it off in the near term. If the carrying cost is low (the code is stable and rarely touched), accept that carrying the debt is cheaper than paying it off. Remove debt items from the backlog when their carrying cost is too low to justify tracking them. |
中文
技术债是软件开发中采取捷径所累积的成本,而且这些成本最终必须偿还。当团队选择快速方案而不是正确方案时——用 global variable 替代 proper state management,复制代码而不是抽取成 reusable module,因为明天就是 deadline 而在没有 automated tests 的情况下部署——工作会更快完成,但未来工作会变慢。这个隐喻由 Ward Cunningham 于 1992 年提出,来自金融债务:借来的钱让你现在能做原本做不起的事,但代价是未来要支付利息,降低未来投资能力。技术债类似:借来的简单性让你现在能够 ship,但代价是之后开发速度下降。
债务隐喻有用,但不完整。金融债务可以量化;技术债不能。你可以衡量自己欠银行多少钱;但你无法精确衡量一个缺乏架构关注而逐渐生长出来的 codebase 所累积的不顺手程度。金融债务按已知利率产生利息;技术债的持有成本则取决于它位于 codebase 的什么位置、那部分 code 被触碰得多频繁,以及系统需求如何变化。但这个隐喻捕捉到了一件重要事情:当下的捷径会减少未来能力;债务持有越久,偿还成本就越高。不断积累 debt 而从不偿还的系统,最终会达到一种状态:进一步开发慢到系统必须被重写或放弃。
理解技术债——它由什么产生,如何复利化,如何管理,什么时候是合理选择,什么时候是紧急问题——对任何工作时间跨度超过单个 release 的软件实践者都是必要的。而这包括多数实践者。采取捷径的压力真实且持续存在;管理其后果的工具是具体且可学习的;而区分 acceptable debt 与 dangerous debt 的判断力,需要本节提供的概念框架。
前置知识:架构(§7.1)——技术债经常表现为架构问题。测试(§7.2)——tests 使 refactoring 变得安全。软件过程(§7.3)——债务管理是一种过程关切。编程(§2.1)——你需要读懂并修改代码,才能 refactor 它。
从借来的简单性到软件演化经济学
Ward Cunningham 于 1992 年创造 “technical debt” 一词,当时背景是他在 Casio 构建的一个金融软件系统。他在一次演讲中描述了这种情况:团队为了快速 ship,没有花时间获得完整理解,结果系统可以工作,但在继续添加功能之前需要 refactor。他明确把这表述为债务:你通过做一些自己知道并不完全正确的事情,向未来借款;然后通过 refactoring 偿还它。
Cunningham 使用这个隐喻,是为了向非技术 stakeholders 沟通——解释为什么团队需要时间在添加新功能前“清理”代码。这个隐喻有效,因为金融 stakeholders 理解债务。它也有效,因为它捕捉到了真实关系:现在的速度与未来的速度之间,通过 code quality 发生中介。
这个概念传播后发生了分叉。Cunningham 的原始用法描述的是一种特定的 intentional debt:代码是在理解尚不完全的情况下写出的,并计划在获得理解后 refactor。这有时被称为 “deliberate, prudent” debt——团队知道自己在走捷径,并计划偿还。Martin Fowler 2009 年的 Technical Debt Quadrant 增加了另外三类:reckless debt(没有意识到后果而采取的捷径)、inadvertent debt(只有回头看、团队学到更多之后才显现的问题),以及 deliberateness 与 recklessness 的组合。真实 codebases 往往包含全部四种类型,而且系统不同部分的比例不同。
Martin Fowler 的 Refactoring: Improving the Design of Existing Code(1999)为偿还 debt 的活动提供了技术词汇。Refactoring 是在不改变 observable behavior 的情况下重构既有代码的过程——相同 inputs 产生相同 outputs,但内部结构被改进。Fowler 编目了六十多个命名 refactorings:Extract Method(把一段 code 从一个 method 中取出,放入一个有名称的新 method)、Move Method(把 method 移到最常使用它的 class 中)、Rename(为 variable、method 或 class 选择更好的名字)、Introduce Parameter Object(用一个 object 替代一组相关 parameters)。这个 catalog 对 refactoring 所做的事,类似 Gang of Four 的 patterns 对 design 所做的事:给实践者提供共享词汇和常见操作的参考。
Refactoring 的关键伴侣是 automated tests。没有 tests,refactoring 就是 reckless:你重构代码,却没有办法验证 behavior 没有改变。有了 tests,refactoring 才是安全的:每个小 transformation 都可以立即被验证。Test suite 使“相同行为”这一保证在实践中可实现。这就是 test-driven development 与 refactoring 一起演化的原因——二者是同一纪律的两个方面。Red-Green-Refactor cycle 使 refactoring 成为让 tests pass 之后的第三个强制步骤:不只是 “make it work”,还要 “make it clean”。
Michael Feathers 的 Working Effectively with Legacy Code(2004)处理的是那些缺少 test coverage、因而无法安全 refactoring 的系统这一具体挑战。Feathers 对 “legacy code” 的定义——没有 tests 的 code——有意挑衅:它通过给 legacy code 一个技术定义和解决路径,使问题变得可处理。书中的策略——characterization tests(记录当前行为而非理想行为的 tests)、seam identification(找到可以在不直接修改代码的情况下改变行为的位置),以及各种 dependency-breaking techniques——提供了向既有系统逐步添加 coverage 的工具,使 refactoring 变得可能。
Manny Lehman 的 software evolution laws 来自他对 1970 年代 IBM 软件的经验研究,并在 1990 年代持续完善,为理解软件系统如何随时间变化提供了经验基础。最重要的 laws 包括:一个软件系统一旦发布,就必须持续适应,否则会逐渐变得不令人满意(law of continuing change);随着系统演化,其复杂度会增加,除非有工作专门减少它(law of increasing complexity);系统增长速度有自我调节倾向(law of self-regulation)。Lehman 的 laws 描述了未被管理的系统会发生什么——它们会积累复杂性、变慢,并逐渐变得不令人满意。对抗 Lehman laws 的管理工作,就是技术债管理。
技术债的经济学 framing,由 Steve McConnell 的 “Managing Technical Debt”(2007)以及后来多位研究者关于量化 technical debt carrying cost 的工作做了最系统处理。关键洞见是,技术债有 principal(修复问题的成本)和 interest(因为问题未修复而在未来工作中额外花费的时间)。一个 poorly factored authentication module 如果每次修改都需要正常情况下两倍时间,那么每次 feature 触及 authentication 时,它都会产生半个 developer-day 的 interest。如果 authentication 每月被触碰五次,carrying cost 就是每月 2.5 个 developer-days——远高于 refactor 这个 module 的成本。只要 carrying cost 超过 principal,refactoring 的经济账就是有利的;那些在 frequently-touched code 中持有昂贵 debt 的系统,开发成本会系统性高于 well-factored code 的系统。
当代技术债管理图景已经被 static analysis tools 改变。这些工具可以识别与 debt 相关的特定 patterns:code duplication(SonarQube 和类似工具会测量)、cyclomatic complexity(与 testing difficulty 相关)、coupling metrics(揭示架构问题),以及 code churn rate(与代码被修改频率相关,因而也与其质量重要性相关)。AI-assisted refactoring tools 可以建议具体 refactorings,识别 dead code,并生成使 refactoring 安全的 characterization tests。这些工具降低了 debt identification 的成本;但优先偿还哪些 debt,以及如何偿还,仍然是人类判断。
代码经济学、Refactoring 实践与何时重写
Technical Debt 的成本模型
技术债具有经济结构,这种结构决定了什么时候偿还它值得,什么时候继续持有它才是正确选择。
一个 debt item 的 principal 是修复它的成本:refactor code、添加缺失 tests 或重新设计 architecture 所需的 effort。Interest 是 debt 持续施加的 ongoing cost:绕过令人困惑的 abstraction 所需的额外时间,duplicated code 必须保持同步而引入的额外 bugs,developers 在修改复杂 module 前必须理解它所产生的额外 cognitive load。
Interest rates 会因 debt 所在位置而发生巨大变化。很少被触碰的 code 中的 debt 具有低 interest:如果六个月内没人修改 authentication module,它的 poor factoring 在这六个月中不产生成本。频繁修改的 code 中的 debt 具有高 interest:如果 data model 每周都被修改,它的 awkward design 每周都会产生成本。Code churn rate——代码被修改得多频繁——是 debt interest rate 的主要决定因素。把 debt paydown 聚焦在 frequently-modified code 上,会产生最高回报。
Debt 也会与 defect rate 互动。Poorly factored code 更难理解,因此也更难正确修改;与实现相同行为的 well-factored code 相比,poorly factored code 有更高 defect rates。额外 defect cost——用于发现、诊断和修复 bugs 的时间——是 debt 累积的另一种 interest。高技术债系统会系统性拥有更高 defect rates,这既是经济成本(修 bug 很贵),也是质量成本(defects 影响 users)。
按 deliberateness(团队是否知道自己在走捷径)和 prudence(这个捷径是否合理)组织 debt types 的 quadrant,可以澄清什么时候 debt 是正当的。Deliberate and prudent debt——“我们知道这不是正确设计,但我们现在先 ship,以验证市场;一旦知道 product viable,我们就 refactor”——是一种有效商业决策。Reckless and inadvertent debt——因为团队没有注意、没有足够能力知道更好做法,或处在不允许关注质量的压力下而累积的 debt——纯粹是成本。多数真实 codebases 都包含混合类型;区分这些类型,决定了 remediation strategy 应该是什么。
Refactoring:受控变化的实践
Refactoring 是没有 behavioral change 的 structural improvement。这个约束——refactoring 后,相同 inputs 产生相同 outputs——正是使 refactoring 安全的原因:它不是 functional change,所以不应引入新 bugs(并由 tests 验证该约束)。Refactoring 的目的,是改善 code 的内部结构,使未来 changes 更容易、更快,也更不容易引入 bugs。
Refactoring 纪律需要几个支撑条件。第一,tests:每次 refactoring 都应由 automated tests 验证 behavior 没有改变。没有 tests 就开始 refactoring,就是没有 safety net 就开始;不可避免的错误无法被自动抓住。第二,small steps:每个单独 refactoring transformation 都应小到明显安全。一次性重构系统许多部分的大型 refactorings 很危险,因为多种 changes 的组合可能以意外方式互动。比如 Extract Method refactoring 是安全的,因为它把边界清楚的 code 移到一个有清晰名称的新 method 中;改变 observable behavior 的风险低。同时重设计跨五十个 modules 的 data model 不是 refactoring;那是 rewrite。
Refactorings catalog 提供了思考 structural improvement 的词汇。除了单个命名 refactorings 之外,更高层的 refactoring goals——reduce duplication、improve naming、reduce class size、reduce method length、clarify dependencies、invert control——提供了 refactoring effort 的方向。Boy scout rule——让 code 比你发现它时稍微干净一点——是使 continuous refactoring 不需要 dedicated refactoring sprints 也可行的纪律:每次 developer 在某个 module 中工作,就让它稍微变好,因此持续开发的累积效果是改善,而不是退化。
Opportunistic refactoring——在开发某个 feature 并触碰相关 code 时顺手 refactor——比 scheduled refactoring 更可持续。一个专门的 “refactoring sprint” 会让团队从 feature delivery 中抽离一个 sprint,使 stakeholders 看到 feature delivery 变慢而产生抵触,并且以很可能追不上 debt 累积速度的方式偿还 technical debt。Opportunistic refactoring 处理的是正在被触碰的 code 中的 debt,其成本在 feature delivery time 中不可见:feature delivery time 包含了 refactoring time,但 refactoring 会减少同一区域后续 features 的时间。
Rewrite 问题
技术债累积到一定程度后,最终会提出一个问题:继续 incremental refactoring,还是从零 rewrite。Joel Spolsky 2000 年文章 “Things You Should Never Do” 强烈反对 rewrites:既有 code 无论多丑,都编码了数十年的 bug fixes 和 edge case handling,这些内容在 code 中未必可见,但在 rewrite 中会痛苦地重新发现。Rewrites 很少按时交付,经常无法复制原系统的完整行为,并且在 rewrite 进行期间,会让组织失去继续开发原系统的能力。
支持 rewrites 的论点是:某些 codebases 深深围绕错误假设组织,incremental refactoring 触及不到问题——带着从原系统积累的知识从零重新设计,可能更便宜、更快。这个论点有时是正确的;确实存在一些系统,其 architecture 根本错误到任何 incremental improvement 都无法使其变得 maintainable。
Strangler fig pattern(Martin Fowler,2004)提供了综合方案:通过在 legacy system 旁边构建一个新系统,并随着新系统建设,把特定功能路由到新系统中,逐步替换 legacy system,直到原系统被完全替代。这种方法提供了 redesign 的收益——更干净的 architecture、更好的理解——同时维持 operational continuity。旧系统继续运行,并处理尚未迁移的功能;新系统则在旁边成长。这个隐喻来自 strangler fig tree:它围绕宿主树生长,并在宿主死亡时逐渐替代它。
关于 rewrite 的技术和经济分析应当具体:当前系统的 ongoing carrying cost 是多少?rewrite 需要什么?rewrite 失败或错过 deadline 的风险是什么?决策不应由 developer aesthetics 驱动——“我们想用新 framework”——而应由当前系统 carrying cost 与 replacement 成本和风险之间的经济关系驱动。
学习这一部分会改变什么
技术债与 refactoring 会改变实践者对代码的理解:代码不是短期产物,而是长期资产。
第一个变化,是能够显性说明捷径的成本。当团队处于压力下想走捷径时,理解 technical debt 的实践者可以说:“这个捷径会给这个 module 每月增加约 X 个 developer-days 的 carrying cost。偿还它需要 Y 个 developer-days。考虑到我们频繁触碰这个 module,我们应该计划在 Z 周内偿还它。”这会把“我们以后应该清理一下”这种模糊感,转化为带有具体成本和时间线的 business argument。
第二个变化,是拥有 prioritizing refactoring 的词汇。并非所有技术债都同样昂贵;frequently-modified code 中的 debt,比 stable code 中的 debt 产生更高 interest。理解 debt economics 的实践者,可以基于 code churn 来确定 refactoring 优先级——把 refactoring effort 聚焦在最常被修改、因而以糟糕状态持有成本最高的 code 上。
第三个变化,是有信心进行 structural changes。学习过 refactoring,并保持良好 test coverage 的实践者,可以带着信心修改既有代码结构——不是鲁莽的信心,而是校准过的信心:每个小 refactoring step 都被 tests 验证,而许多安全小步骤的累积可以产生大规模改善,并避免大规模 rewrite 的风险。这种信心降低了改善 code quality 的成本,因此也使它更可能发生。
第四个变化,是能够判断 legitimate debt 与 dangerous debt。为了 ship 一个验证产品方向的 feature 而承担 deliberate, prudent debt,是合理商业决策。因为团队没有时间把事情做对而不断累积 reckless debt,则是关于团队工作条件的 red flag。能够做出这一区分的实践者,可以在技术债策略讨论中有效参与,而不是要么无视 debt concerns,要么对 code quality 过度灾难化。
资源
书籍与文本
Fowler 的 Refactoring: Improving the Design of Existing Code(第 2 版,Addison-Wesley,2018)是奠基文本。第二版使用 JavaScript examples 更新了 catalog,同时保持第一版的概念框架。阅读 Part I(第 1–3 章,包括 worked example、principles、code smells)可以获得必要概念框架;后续 refactorings catalog 是按需查阅的参考材料。这个 catalog 也有旧版在线版本,可在 refactoring.guru 查阅。
Feathers 的 Working Effectively with Legacy Code(Prentice Hall,2004)是改善缺少 test coverage 代码的实践指南,而 test coverage 正是安全 refactoring 的前提。其 dependency-breaking techniques 和 characterization testing approach,是解决“为未测试 legacy code 添加 coverage”这一具体问题的具体工具。没有其他书如此直接、完整地处理这个问题。
Beck 的 Test-Driven Development: By Example(§7.2 参考)提供 Red-Green-Refactor cycle,使 refactoring 成为一种持续实践,而不是周期性 cleanup activity。
McConnell 的 Code Complete(第 2 版,Microsoft Press,2004)为 code quality decisions 提供更宽泛语境,覆盖 construction practices、variable and function naming、code organization 和 defensive programming。其关于 design 的章节尤其与 technical debt 相关:construction time 做出的决策,可能后来很难改变。
Richardson 的 Microservices Patterns: With Examples in Java(Manning,2018)处理的是由需要分解的 monolithic architectures 造成的特定技术债形式——这是当代软件中最常见的大规模 refactoring 挑战。
| 书籍 | 作用 | 类型 |
|---|---|---|
| Fowler, Refactoring(第 2 版) | Refactoring 奠基文本 | 深入 |
| Feathers, Working Effectively with Legacy Code | Legacy code 改善 | 深入 |
| Refactoring-loop reference: Beck, Test-Driven Development: By Example | Red-green-refactor 纪律 | 深入 |
| McConnell, Code Complete(第 2 版) | Construction quality 基础 | 深入 |
| Richardson, Microservices Patterns | Monolith decomposition patterns | 深入 |
| Tornhill, Software Design X-Rays | Behavioral code analysis 与 hotspot-based debt management | 深入 |
课程、论文与当前资料
Cunningham 的 original 1992 OOPSLA talk,也就是 technical debt 隐喻首次提出的地方,以及他后来的 clarifications,都可以通过 Ward Cunningham wiki(c2.com)获取。阅读原始 framing——为了日后 refactor 而有意承担的 debt,而不是随意欠下的 debt——可以补足二手叙述经常漏掉的语境。
Fowler 的 “Technical Debt Quadrant” blog post(2009,可在 martinfowler.com 免费获取)是 quadrant categorization 的奠基性二手来源。他在 refactoring.com 的 refactorings catalog,则提供了书中 catalog 的在线参考版本。
Spolsky 的 “Things You Should Never Do, Part I”(2000,可在 joelonsoftware.com 免费获取)和 Fowler 的 “Strangler Fig Application”(2004,可在 martinfowler.com 免费获取)是 rewrite debate 的一手文本。
| 资源 | 平台 | 类型 |
|---|---|---|
| Cunningham, c2.com technical debt pages(免费) | c2.com | 深入 |
| Fowler, “Technical Debt Quadrant”(免费) | martinfowler.com | 参考 |
| Fowler, “Strangler Fig Application”(免费) | martinfowler.com | 参考 |
| Spolsky, “Things You Should Never Do”(免费) | joelonsoftware.com | 深入 |
| Code Red code quality study(免费) | arXiv | 参考 |
实践、工具与当前资料
SonarQube(community edition 免费,sonarqube.org)会分析与 technical debt 相关的特定 patterns:duplication、complexity、code smells、security vulnerabilities。在既有 codebase 上运行 SonarQube,并检查 debt report——哪些 files complexity 最高,哪些 duplication 最多——是最快看到 technical debt measurement 实际样子的方式。
Code Climate(商业,有免费层)提供类似分析,并增加 code churn 指标:哪些 files 被最频繁修改。Complexity 与 churn 的组合,可以识别最高优先级 refactoring targets——也就是既 messy、又经常被修改的 code。
refactoring.guru catalog 为每个 refactoring pattern 提供带有 before/after examples 的可视化说明。将它与 Fowler 的书一起使用,会让 catalog 更易进入;这种视觉形式比单纯文字描述更清楚地展示 structural change。
在 IDE 中执行 refactoring——使用 IDE 内置 refactoring tools(rename、extract method、move class),而不是手工编辑文本——是发展 refactoring fluency 的最高效方式。IDE 的 refactoring tools 会安全应用 transformation(例如处理 renamed symbol 的全部 references),并验证结果可以 compile;它们是 “small safe steps” 纪律的实践实现。
| 资源 | 平台 | 类型 |
|---|---|---|
| SonarQube Community(免费) | sonarqube.org | 实践 |
| Code Climate(免费层) | codeclimate.com | 实践 |
| CodeScene(商业;free trial/options may vary) | codescene.io | 实践 |
| OpenRewrite(免费;Moderne platform 付费) | openrewrite.org / GitHub | 实践 |
| Renovate(开源;hosted options 可能付费) | docs.renovatebot.com | 实践 |
| refactoring.guru(免费) | refactoring.guru | 参考 |
| IDE refactoring tools(内置) | IntelliJ / VS Code / Eclipse | 实践 |
陷阱
| 陷阱 | 为什么会误导 | 更好的回应 |
|---|---|---|
| 把 technical debt 一律视为坏事 | Technical debt 有时是合理商业决策。为了 ship 一个验证产品方向的 feature 而承担 deliberate, prudent debt,和因为粗心累积 reckless debt 不同。把所有 technical debt 都视为同样糟糕,会混淆二者,使 debt discussions 变得低效(因为 developers 会觉得自己在做出合理 tradeoffs 时被指责为实践糟糕),并导致那些捷径确实是正确选择的情境中出现 over-engineering。 | 使用 debt quadrant 区分 debt types。对 deliberate debt,明确说明 pay it off 的意图,并进行跟踪。对 inadvertent debt,诊断它如何累积,以防复发。对 reckless debt,处理产生它的条件。讨论某个 shortcut 是 prudent 还是 reckless 是有生产力的;笼统判断 shortcuts 永远错误则不是。 |
| Scheduled “refactoring sprints” 而不是 continuous refactoring | 专门拿一个 sprint 做 refactoring,会(a)降低 stakeholders 能看到的 feature velocity,制造组织阻力;(b)只在某个时间点处理 debt,而不是持续处理,因此 debt 会在 sprints 之间积累;(c)很少能跟上新 debt 累积速度。周期性 refactoring sprints 的团队通常会发现,debt 增长得比 sprints 能处理的速度更快。 | 实践 boy scout rule:每次触碰 code,都让它比你发现它时稍微好一点。Opportunistic refactoring——在相关代码中工作时顺手改善它——不会在 feature delivery timelines 中显眼出现,也不会制造组织阻力。持续小改进的总效果,会以更低组织成本超过周期性大型 refactoring efforts 的效果。 |
| 没有 tests 就 refactor | 没有 automated tests 验证 behavior 没有改变的 refactoring,不是 refactoring,而是 restructuring。对代码进行任何结构变化都有引入 bugs 的风险;没有 tests,这些 bugs 要到 production 中出现才会被发现。那些没有 tests 却“refactor”的团队,经常在相信自己只是改善结构时引入 bugs。 | Refactoring 前建立 test coverage。如果 coverage 不足,使用 Feathers 所描述的 characterization tests,在改变结构前记录当前 behavior。对 characterization tests 的投入,会立即转化为 refactoring 没有改变 behavior 的信心;这些 tests 也会继续为未来 changes 提供价值。 |
| Rewrites 中的 second-system effect | 重写 legacy system 的团队很少是在构建 system 2;他们是在第二次构建第一个系统,并且带着“重新开始”的自由所激发的全部 over-engineering 冲动。Rewrites 通常比估计更久,经常无法复制原系统的所有 behavior(直到 rewrite 部署后才发现缺失 requirements),并在原系统已经演化出正确性的地方引入新 bugs。 | 承诺 rewrite 前,明确考虑 second-system effect:那种增加 features、试图“这次做对”的倾向,会延长 timelines。尽可能使用 strangler fig approach——incrementally migrate,一块一块替换原系统,同时保持 operational continuity。如果 complete rewrite 确实必要,就严格限定 scope:先复制 existing behavior,把 improvements 推迟到 migration 完成之后。 |
| Debt tracking without paydown | 许多团队会在 issue trackers 中跟踪 technical debt,但并不真正处理它。Debt backlog 不断增长,items 不断老化,而“跟踪”这个动作提供了“我们知道这个问题”的组织安慰,却没有实际减少 debt。被跟踪但从未偿还的 debt,与未被跟踪的 debt 成本相同;tracking 只增加 overhead 而没有收益。 | 对每个被跟踪的 debt item,评估其 carrying cost。如果 carrying cost 显著(code 被频繁修改,而且 debt 使修改变慢),就在近期优先偿还。如果 carrying cost 很低(code 稳定且很少触碰),就接受继续持有 debt 比偿还更便宜。当 debt item 的 carrying cost 低到不值得跟踪时,将其从 backlog 中移除。 |