技术债务评估
概览
系统地识别、量化和管理技术债务,以便对代码质量投资做出明智的决策。
何时使用
- 遗留代码评估
- 重构优先级
- 冲刺计划
- 代码质量计划
- 收购尽职调查
- 架构决策
实施示例
1. 技术债务计算器
interface DebtItem {
id: string;
title: string;
description: string;
category: 'code' | 'architecture' | 'test' | 'documentation' | 'security';
severity: 'low' | 'medium' | 'high' | 'critical';
effort: number; // 小时
impact: number; // 1-10 规模
interest: number; // 如果不修复,每个冲刺的成本
}
class TechnicalDebtAssessment {
private items: DebtItem[] = [];
addDebtItem(item: DebtItem): void {
this.items.push(item);
}
calculatePriority(item: DebtItem): number {
const severityWeight = {
low: 1,
medium: 2,
high: 3,
critical: 4
};
const priority =
(item.impact * 10 + item.interest * 5 + severityWeight[item.severity] * 3) /
(item.effort + 1);
return priority;
}
getPrioritizedList(): Array<DebtItem & { priority: number }> {
return this.items
.map(item => ({
...item,
priority: this.calculatePriority(item)
}))
.sort((a, b) => b.priority - a.priority);
}
getDebtByCategory(): Record<string, DebtItem[]> {
return this.items.reduce((acc, item) => {
acc[item.category] = acc[item.category] || [];
acc[item.category].push(item);
return acc;
}, {} as Record<string, DebtItem[]>);
}
getTotalEffort(): number {
return this.items.reduce((sum, item) => sum + item.effort, 0);
}
getTotalInterest(): number {
return this.items.reduce((sum, item) => sum + item.interest, 0);
}
generateReport(): string {
const prioritized = this.getPrioritizedList();
const byCategory = this.getDebtByCategory();
let report = '# 技术债务评估
';
// 摘要
report += '## 摘要
';
report += `- 总项数: ${this.items.length}
`;
report += `- 总工作量: ${this.getTotalEffort()} 小时
`;
report += `- 每月利息: ${this.getTotalInterest()} 小时
`;
// 按类别
report += '## 按类别
';
for (const [category, items] of Object.entries(byCategory)) {
const effort = items.reduce((sum, item) => sum + item.effort, 0);
report += `- ${category}: ${items.length} 项 (${effort} 小时)
`;
}
report += '
';
// 优先级最高的项
report += '## 优先级最高的项
';
for (const item of prioritized.slice(0, 10)) {
report += `### ${item.title} (优先级: ${item.priority.toFixed(2)})
`;
report += `- 类别: ${item.category}
`;
report += `- 严重性: ${item.severity}
`;
report += `- 工作量: ${item.effort} 小时
`;
report += `- 影响: ${item.impact}/10
`;
report += `- 利息: ${item.interest} 小时/冲刺
`;
report += `
${item.description}
`;
}
return report;
}
}
// 使用方法
const assessment = new TechnicalDebtAssessment();
assessment.addDebtItem({
id: 'debt-1',
title: '遗留 API 端点',
description: '仍在使用的旧 API v1 端点,需要迁移',
category: 'architecture',
severity: 'high',
effort: 40,
impact: 8,
interest: 5
});
assessment.addDebtItem({
id: 'debt-2',
title: '缺少单元测试',
description: '30% 的代码库缺少测试覆盖',
category: 'test',
severity: 'medium',
effort: 80,
impact: 7,
interest: 3
});
console.log(assessment.generateReport());
2. 代码质量扫描器
import * as ts from 'typescript';
import * as fs from 'fs';
interface QualityIssue {
file: string;
line: number;
issue: string;
severity: 'info' | 'warning' | 'error';
debtHours: number;
}
class CodeQualityScanner {
private issues: QualityIssue[] = [];
scanProject(directory: string): QualityIssue[] {
this.issues = [];
const files = this.getTypeScriptFiles(directory);
for (const file of files) {
this.scanFile(file);
}
return this.issues;
}
private scanFile(filePath: string): void {
const sourceCode = fs.readFileSync(filePath, 'utf-8');
const sourceFile = ts.createSourceFile(
filePath,
sourceCode,
ts.ScriptTarget.Latest,
true
);
// 检查反模式
this.checkForAnyTypes(sourceFile, filePath);
this.checkForLongFunctions(sourceFile, filePath);
this.checkForMagicNumbers(sourceFile, filePath);
this.checkForConsoleStatements(sourceFile, filePath);
this.checkForTodoComments(sourceFile, filePath);
}
private checkForAnyTypes(sourceFile: ts.SourceFile, filePath: string): void {
const visit = (node: ts.Node) => {
if (ts.isTypeReferenceNode(node) && node.typeName.getText() === 'any') {
const { line } = ts.getLineAndCharacterOfPosition(
sourceFile,
node.getStart()
);
this.issues.push({
file: filePath,
line: line + 1,
issue: '使用 any 类型降低了类型安全性',
severity: 'warning',
debtHours: 0.5
});
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
}
private checkForLongFunctions(sourceFile: ts.SourceFile, filePath: string): void {
const visit = (node: ts.Node) => {
if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) {
if (node.body) {
const lines = node.body.getFullText().split('
').length;
if (lines > 50) {
const { line } = ts.getLineAndCharacterOfPosition(
sourceFile,
node.getStart()
);
this.issues.push({
file: filePath,
line: line + 1,
issue: `函数有 ${lines} 行,应该重构`,
severity: 'warning',
debtHours: Math.ceil(lines / 10)
});
}
}
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
}
private checkForMagicNumbers(sourceFile: ts.SourceFile, filePath: string): void {
const visit = (node: ts.Node) => {
if (ts.isNumericLiteral(node)) {
const value = parseFloat(node.text);
// 忽略常见常数
if (![0, 1, -1, 2].includes(value)) {
const { line } = ts.getLineAndCharacterOfPosition(
sourceFile,
node.getStart()
);
this.issues.push({
file: filePath,
line: line + 1,
issue: `魔术数字 ${value} 应该是一个命名常量`,
severity: 'info',
debtHours: 0.1
});
}
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
}
private checkForConsoleStatements(sourceFile: ts.SourceFile, filePath: string): void {
const text = sourceFile.getFullText();
const lines = text.split('
');
lines.forEach((line, index) => {
if (line.includes('console.log') || line.includes('console.error')) {
this.issues.push({
file: filePath,
line: index + 1,
issue: '控制台语句应该使用适当的日志记录器',
severity: 'info',
debtHours: 0.1
});
}
});
}
private checkForTodoComments(sourceFile: ts.SourceFile, filePath: string): void {
const text = sourceFile.getFullText();
const lines = text.split('
');
lines.forEach((line, index) => {
if (/\/\/\s*TODO/.test(line)) {
this.issues.push({
file: filePath,
line: index + 1,
issue: 'TODO 注释表明工作不完整',
severity: 'warning',
debtHours: 2
});
}
});
}
private getTypeScriptFiles(dir: string): string[] {
// 实现
return [];
}
getTotalDebt(): number {
return this.issues.reduce((sum, issue) => sum + issue.debtHours, 0);
}
generateReport(): string {
let report = '# 代码质量报告
';
const bySeverity = this.issues.reduce((acc, issue) => {
acc[issue.severity] = acc[issue.severity] || [];
acc[issue.severity].push(issue);
return acc;
}, {} as Record<string, QualityIssue[]>);
report += `## 摘要
`;
report += `- 总问题数: ${this.issues.length}
`;
report += `- 估计债务: ${this.getTotalDebt()} 小时
`;
for (const [severity, issues] of Object.entries(bySeverity)) {
report += `### ${severity.toUpperCase()} (${issues.length})
`;
for (const issue of issues.slice(0, 10)) {
report += `- ${issue.file}:${issue.line} - ${issue.issue}
`;
}
report += '
';
}
return report;
}
}
最佳实践
✅ 要做
- 量化债务影响
- 按 ROI 优先级排序
- 跟踪债务随时间变化
- 将债务包含在冲刺中
- 记录债务决策
- 设置质量门
❌ 不要
- 忽视技术债务
- 一次性修复所有问题
- 跳过影响分析
- 做出情绪化的决策
债务类别
- 代码质量: 复杂代码,重复
- 架构: 糟糕的设计,紧密耦合
- 测试: 缺少测试,不稳定的测试
- 文档: 缺少文档,过时
- 安全: 漏洞,过时依赖
- 性能: 低效代码,N+1 查询