DRY 标准化流程:从识别到重构
这个技能指导你系统性地应用 DRY (Don't Repeat Yourself) 原则,通过四步标准化流程消除代码重复,提升代码质量和可维护性。
When to Use This Skill
使用此技能当用户请求:
消除代码重复或冗余 重构有明显复制粘贴痕迹的代码 应用 DRY 原则优化代码库 识别并修复"代码坏味道"(如魔术数字、重复逻辑) 提取公共逻辑为可复用单元 改善代码的可维护性
关键触发词: DRY, 重复代码, 代码重复, 重构, 消除重复, 复制粘贴, 魔术数字, 代码坏味道, 抽象, 提取函数
核心思想
系统中的每一处知识都必须拥有一个单一、明确、权威的表示。
这意味着:
任何业务逻辑、算法或配置信息都应该只存在于代码库的一个地方 如果需要修改,你只需改这一个地方 修改会自动反映到所有使用该逻辑的地方
两次法则 (Rule of Two): 当你第二次写下几乎相同的代码块时,警钟就应该敲响。这是开始重构的信号。
四步标准化流程
这是一个可在编码任何阶段应用的微循环。严格按照步骤执行,确保重构的安全性和有效性。
第一步:识别重复 (Identify Repetition)
目标: 像侦探一样,对代码中的"坏味道"保持警惕,找出所有重复。
1.1 明显的重复
直接复制粘贴:
两块或多块代码长得几乎一模一样 只有变量名或少数值不同 这是最明显、最需要被消除的重复
示例:
// 重复 1 function calculateOrderDiscount(orderTotal) { if (orderTotal > 100) { return orderTotal * 0.1; } return 0; }
// 重复 2 function calculateCouponDiscount(couponTotal) { if (couponTotal > 100) { return couponTotal * 0.1; } return 0; }
"魔术数字"或字符串:
同一个配置值或字符串在多处以字面量形式出现 例如:0.08、"http://api.example.com"、100
示例:
魔术数字重复
def calculate_tax_1(amount): return amount * 0.08 # ❌ 魔术数字
def calculate_tax_2(amount): return amount * 0.08 # ❌ 再次出现
def calculate_total(amount): tax = amount * 0.08 # ❌ 第三次 return amount + tax
1.2 语义上的重复
结构性重复:
代码结构相似,但具体变量名或值不同 多个 if-else 结构都在做类似的条件判断和赋值
示例:
// 结构性重复 function processUserData(user: User) { if (user.age >= 18) { user.status = 'adult'; } else { user.status = 'minor'; } }
function processProductData(product: Product) { if (product.price >= 100) { product.category = 'premium'; } else { product.category = 'standard'; } }
逻辑重复:
两个不同的函数,代码看起来不一样 但它们在业务逻辑层面实现的是同一个目标
示例:
// 逻辑重复:都在计算折扣,只是来源不同 function applyMembershipDiscount(price, memberLevel) { const discountRates = { gold: 0.2, silver: 0.1, bronze: 0.05 }; return price * (1 - (discountRates[memberLevel] || 0)); }
function applySeasonalDiscount(price, season) { const discountRates = { winter: 0.2, spring: 0.1, summer: 0.05 }; return price * (1 - (discountRates[season] || 0)); }
识别清单
当你审查代码时,检查以下信号:
复制粘贴的代码块(完全相同或高度相似) 相同的数字、字符串在多处出现 相似的 if-else 或 switch-case 结构 功能相似但命名不同的函数 相同的算法在不同地方重新实现 相同的验证逻辑分散在多个地方
💡 Action: 使用搜索功能查找重复的字面量、相似的函数名模式。记录所有重复出现的位置。
第二步:抽象逻辑 (Abstract the Logic)
目标: 将重复的逻辑提取出来,封装到一个独立、可复用的单元中。
2.1 识别可变与不变部分
不变部分:
这是重复的核心逻辑 每次重复时都保持不变的代码 这将成为你的抽象主体
可变部分:
每次重复时发生变化的东西 不同的值、变量名、配置 这些将成为函数或类的参数
分析示例:
// 原始重复代码 const userEmail = validateEmail(user.email); const adminEmail = validateEmail(admin.email); const supportEmail = validateEmail(support.email);
// 分析: // 不变部分:validateEmail() 调用 // 可变部分:不同的 email 值
2.2 选择合适的抽象形式
根据重复的特点,选择最合适的抽象方式:
抽象形式 适用场景 示例 函数 (Function) 封装一段算法或行为 计算折扣、验证输入、格式化数据 类 (Class) 封装行为 + 关联状态 用户管理器、数据处理器、配置管理器 模块/组件 一组相关的函数、类和配置 认证模块、日志模块、API 客户端 配置文件/常量 重复的魔术数字或字符串 API 端点、税率、阈值 高阶函数 重复的控制流程或模式 重试逻辑、缓存包装、错误处理 2.3 设计抽象接口
函数抽象示例:
❌ 重复代码
def process_user_order(user_id, order_data): user = db.query(User).filter_by(id=user_id).first() if not user: raise ValueError("User not found") # 处理订单...
def process_user_payment(user_id, payment_data): user = db.query(User).filter_by(id=user_id).first() if not user: raise ValueError("User not found") # 处理支付...
✅ 抽象后
def get_user_or_error(user_id): """不变部分:获取用户并验证""" user = db.query(User).filter_by(id=user_id).first() if not user: raise ValueError("User not found") return user
def process_user_order(user_id, order_data): user = get_user_or_error(user_id) # 可变部分:user_id # 处理订单...
def process_user_payment(user_id, payment_data): user = get_user_or_error(user_id) # 可变部分:user_id # 处理支付...
常量抽象示例:
// ❌ 魔术数字 function calculateTax(amount) { return amount * 0.08; }
function displayTaxInfo(amount) {
console.log(Tax (8%): $${amount * 0.08});
}
// ✅ 抽象为常量 const TAX_RATE = 0.08;
function calculateTax(amount) { return amount * TAX_RATE; }
function displayTaxInfo(amount) {
console.log(Tax (${TAX_RATE * 100}%): $${amount * TAX_RATE});
}
类抽象示例:
// ❌ 重复的状态和行为 const userCache = new Map(); function getUserFromCache(id: string) { /.../ } function setUserInCache(id: string, user: User) { /.../ }
const productCache = new Map(); function getProductFromCache(id: string) { /.../ } function setProductInCache(id: string, product: Product) { /.../ }
// ✅ 抽象为类
class Cache
get(id: string): T | undefined { return this.store.get(id); }
set(id: string, value: T): void { this.store.set(id, value); } }
const userCache = new Cache
抽象设计原则
Do:
✅ 参数化可变部分(值、配置、行为) ✅ 保持接口简单(参数数量 ≤ 4 个) ✅ 使用描述性命名(说明"做什么"而非"怎么做") ✅ 考虑未来的扩展性(但不要过度设计)
Don't:
❌ 创建过于通用的抽象("万能函数") ❌ 过早抽象(只有一次使用时不要抽象) ❌ 忽略性能影响(例如不必要的函数调用开销) ❌ 使用难以理解的抽象(增加认知负担)
💡 Action: 创建一个新的函数、类或配置文件,将"不变部分"放进去,将"可变部分"定义为参数。
第三步:替换实现 (Replace the Implementation)
目标: 用新的抽象单元替换所有旧的重复代码。
3.1 系统性替换
步骤:
定位所有重复点:回到第一步记录的所有位置 逐一替换:删除旧代码,调用新抽象 传入正确参数:确保参数对应正确 保持行为一致:确保替换前后功能完全相同
替换示例:
Before (重复代码):
位置 1: user_service.py
def create_user(data): if not data.get('email'): return {'error': 'Email is required'}, 400 if not data.get('password'): return {'error': 'Password is required'}, 400 # 创建用户...
位置 2: product_service.py
def create_product(data): if not data.get('name'): return {'error': 'Name is required'}, 400 if not data.get('price'): return {'error': 'Price is required'}, 400 # 创建产品...
After (使用抽象):
新抽象: validation_utils.py
def validate_required_fields(data, required_fields): """验证必填字段""" for field in required_fields: if not data.get(field): return {'error': f'{field.capitalize()} is required'}, 400 return None
位置 1: user_service.py (已替换)
def create_user(data): error = validate_required_fields(data, ['email', 'password']) if error: return error # 创建用户...
位置 2: product_service.py (已替换)
def create_product(data): error = validate_required_fields(data, ['name', 'price']) if error: return error # 创建产品...
3.2 处理边缘情况
有时候重复代码之间存在细微差异,需要特殊处理:
策略 1:添加可选参数
// 大部分重复,但有一个地方需要额外日志 function processData(data, options = {}) { // 通用处理...
if (options.enableLogging) { console.log('Processing:', data); }
return result; }
// 使用 processData(data1); // 无日志 processData(data2, { enableLogging: true }); // 有日志
策略 2:回调函数
// 核心流程相同,但中间步骤不同
function processWithCustomStep
// 可变的自定义步骤 const processed = customStep(prepared);
// 后置处理 return finalize(processed); }
// 使用 processWithCustomStep(userData, (user) => validateUser(user)); processWithCustomStep(productData, (product) => enrichProduct(product));
策略 3:保留特殊情况
如果某个重复有本质上的不同,考虑保留它
def process_standard_order(order): return apply_dry_abstraction(order, 'standard')
def process_vip_order(order): # VIP 订单有完全不同的业务逻辑,不强行抽象 # 保留独立实现 pass
替换清单 确认所有重复点都已替换(不要遗漏) 删除旧的重复代码(避免混用新旧方式) 检查导入语句和依赖关系 确保参数顺序和类型正确 处理了所有边缘情况
⚠️ 警告: 如果只替换了一部分,你就创造了另一种不一致,情况可能更糟。确保全部替换或全部不替换。
💡 Action: 使用 IDE 的"查找所有引用"功能,确保没有遗漏任何重复点。
第四步:验证与测试 (Verify and Test)
目标: 确保重构没有破坏任何功能,程序行为在重构前后完全一致。
4.1 单元测试
为你新创建的抽象编写独立的单元测试:
测试覆盖要点:
✅ 正常输入的正确输出 ✅ 边界值测试(空值、最大值、最小值) ✅ 异常输入的错误处理 ✅ 不同参数组合的行为
示例:
抽象函数
def calculate_discount(price, discount_rate): """计算折扣后价格""" if not 0 <= discount_rate <= 1: raise ValueError("Discount rate must be between 0 and 1") return price * (1 - discount_rate)
单元测试
def test_calculate_discount(): # 正常情况 assert calculate_discount(100, 0.1) == 90 assert calculate_discount(100, 0) == 100
# 边界情况
assert calculate_discount(0, 0.5) == 0
assert calculate_discount(100, 1) == 0
# 异常情况
with pytest.raises(ValueError):
calculate_discount(100, 1.5)
with pytest.raises(ValueError):
calculate_discount(100, -0.1)
4.2 集成测试
运行那些覆盖了被修改代码区域的集成测试:
运行特定模块的测试
pytest tests/test_user_service.py pytest tests/test_product_service.py
或运行整个测试套件
npm test pytest
检查要点:
所有测试都通过 没有新的失败或错误 性能没有显著下降 覆盖率没有降低 4.3 手动验证
如果没有自动化测试(或测试覆盖不足),进行手动验证:
验证清单:
启动应用程序,检查是否正常运行 测试被修改的功能(通过 UI 或 API) 检查日志输出是否正常 测试错误场景(无效输入、边界条件) 在不同环境中测试(开发、测试、预发布) 4.4 性能验证
确保抽象没有引入性能问题:
import time
性能测试
def benchmark_function(func, args, iterations=1000): start = time.time() for _ in range(iterations): func(args) end = time.time() return (end - start) / iterations
对比重构前后
old_time = benchmark_function(old_implementation, test_data) new_time = benchmark_function(new_implementation, test_data)
print(f"Old: {old_time:.6f}s, New: {new_time:.6f}s") print(f"Difference: {((new_time - old_time) / old_time * 100):.2f}%")
4.5 代码审查
如果在团队中工作,进行代码审查:
审查要点:
抽象是否合理且易于理解? 命名是否清晰且符合约定? 是否有遗漏的重复点? 是否过度抽象或设计复杂? 文档和注释是否充分?
💡 Action: 运行所有相关测试,确保程序的外部行为在重构前后完全一致。没有测试?现在是编写测试的最佳时机。
完整示例:从头到尾 场景:电商系统的折扣计算 原始代码(存在重复) // order_service.js function calculateOrderTotal(order) { let total = 0; for (const item of order.items) { total += item.price * item.quantity; }
// 会员折扣 if (order.memberLevel === 'gold') { total = total * 0.8; // ❌ 魔术数字 } else if (order.memberLevel === 'silver') { total = total * 0.9; // ❌ 魔术数字 }
return total; }
// cart_service.js function calculateCartTotal(cart) { let total = 0; for (const item of cart.items) { total += item.price * item.quantity; // ❌ 重复计算逻辑 }
// 优惠券折扣 if (cart.couponType === 'premium') { total = total * 0.8; // ❌ 重复的折扣计算 } else if (cart.couponType === 'standard') { total = total * 0.9; // ❌ 重复的折扣计算 }
return total; }
步骤 1:识别重复
发现的重复:
计算商品总价的循环逻辑(结构重复) 折扣计算逻辑(逻辑重复) 魔术数字 0.8 和 0.9(明显重复) 步骤 2:抽象逻辑 // pricing_utils.js (新建)
// 抽象 1:商品总价计算 function calculateItemsTotal(items) { return items.reduce((total, item) => { return total + (item.price * item.quantity); }, 0); }
// 抽象 2:折扣配置(消除魔术数字) const DISCOUNT_RATES = { membership: { gold: 0.2, // 20% off silver: 0.1, // 10% off bronze: 0.05 // 5% off }, coupon: { premium: 0.2, // 20% off standard: 0.1, // 10% off basic: 0.05 // 5% off } };
// 抽象 3:应用折扣 function applyDiscount(amount, discountRate) { if (discountRate < 0 || discountRate > 1) { throw new Error('Invalid discount rate'); } return amount * (1 - discountRate); }
// 抽象 4:获取折扣率 function getDiscountRate(category, level) { return DISCOUNT_RATES[category]?.[level] || 0; }
export { calculateItemsTotal, applyDiscount, getDiscountRate };
步骤 3:替换实现 // order_service.js (重构后) import { calculateItemsTotal, applyDiscount, getDiscountRate } from './pricing_utils.js';
function calculateOrderTotal(order) { const subtotal = calculateItemsTotal(order.items); const discountRate = getDiscountRate('membership', order.memberLevel); return applyDiscount(subtotal, discountRate); }
// cart_service.js (重构后) import { calculateItemsTotal, applyDiscount, getDiscountRate } from './pricing_utils.js';
function calculateCartTotal(cart) { const subtotal = calculateItemsTotal(cart.items); const discountRate = getDiscountRate('coupon', cart.couponType); return applyDiscount(subtotal, discountRate); }
步骤 4:验证与测试 // pricing_utils.test.js import { calculateItemsTotal, applyDiscount, getDiscountRate } from './pricing_utils.js';
describe('Pricing Utils', () => { describe('calculateItemsTotal', () => { it('should calculate total for multiple items', () => { const items = [ { price: 10, quantity: 2 }, { price: 5, quantity: 3 } ]; expect(calculateItemsTotal(items)).toBe(35); });
it('should return 0 for empty items', () => {
expect(calculateItemsTotal([])).toBe(0);
});
});
describe('applyDiscount', () => { it('should apply 20% discount correctly', () => { expect(applyDiscount(100, 0.2)).toBe(80); });
it('should throw error for invalid discount rate', () => {
expect(() => applyDiscount(100, 1.5)).toThrow('Invalid discount rate');
});
});
describe('getDiscountRate', () => { it('should return correct membership discount', () => { expect(getDiscountRate('membership', 'gold')).toBe(0.2); });
it('should return 0 for unknown level', () => {
expect(getDiscountRate('membership', 'unknown')).toBe(0);
});
}); });
// 运行测试 // npm test pricing_utils.test.js
重构成果:
✅ 消除了所有重复代码 ✅ 魔术数字集中管理 ✅ 每个函数职责单一 ✅ 易于测试和维护 ✅ 如果需要添加新的会员等级或折扣类型,只需修改 DISCOUNT_RATES 常见陷阱与解决方案 陷阱 1:过度抽象 (Over-Abstraction)
症状:创建了过于通用、难以理解的抽象。
示例:
// ❌ 过度抽象 function universalProcessor(data, options, callbacks, config, meta) { // 100 行通用处理逻辑... }
// ✅ 合理抽象 function processUserData(user) { return validate(user) && transform(user); }
解决方案:
只在有明确重复时才抽象 保持抽象简单明了 如果参数超过 4 个,考虑重新设计 陷阱 2:不完全替换
症状:只替换了部分重复点,留下了一些旧代码。
后果:
代码库中存在新旧两种实现 未来修改时容易遗漏 造成新的不一致
解决方案:
使用全局搜索确保找到所有重复 一次性完成所有替换 使用 linter 或静态分析工具检测未使用的代码 陷阱 3:忽略性能影响
症状:抽象引入了不必要的性能开销。
示例:
❌ 每次调用都重新编译正则表达式
def validate_email(email): return re.match(r'^[\w.-]+@[\w.-]+.\w+$', email)
✅ 复用编译后的正则表达式
EMAIL_PATTERN = re.compile(r'^[\w.-]+@[\w.-]+.\w+$')
def validate_email(email): return EMAIL_PATTERN.match(email)
解决方案:
对性能敏感的代码进行基准测试 使用缓存、记忆化等优化技术 必要时使用性能分析工具 陷阱 4:破坏封装性
症状:抽象暴露了过多内部实现细节。
示例:
// ❌ 暴露内部状态
class UserManager {
public users: Map
getUser(id: string) { return this.users.get(id); } }
// ✅ 隐藏内部实现
class UserManager {
private users: Map
getUser(id: string): User | undefined { return this.users.get(id); }
addUser(user: User): void { this.users.set(user.id, user); } }
解决方案:
使用访问控制(private, protected) 提供明确的公共接口 隐藏实现细节 最佳实践 1. 渐进式重构
不要试图一次性重构整个代码库:
策略:
✅ 每次重构一个小的重复区域 ✅ 重构后立即测试 ✅ 提交小的、原子性的变更 ✅ 逐步扩大重构范围
示例工作流:
1. 创建特性分支
git checkout -b refactor/dry-pricing-logic
2. 重构一个模块
编辑 pricing_utils.js
3. 测试
npm test
4. 提交
git add pricing_utils.js git commit -m "Extract pricing calculation to reusable utility"
5. 重构使用该模块的文件
编辑 order_service.js
6. 再次测试和提交
npm test git add order_service.js git commit -m "Refactor order service to use pricing utility"
7. 继续其他模块...
- 文档化你的抽象
好的抽象需要好的文档:
/*
* 计算商品折扣后的价格
*
* @param price - 原始价格(必须 >= 0)
* @param discountRate - 折扣率(0-1 之间,0.2 表示 20% 折扣)
* @returns 折扣后的价格
* @throws {Error} 如果 discountRate 不在有效范围内
*
* @example
* applyDiscount(100, 0.2) // 返回 80
* applyDiscount(50, 0) // 返回 50(无折扣)
/
function applyDiscount(price: number, discountRate: number): number {
if (discountRate < 0 || discountRate > 1) {
throw new Error(Invalid discount rate: ${discountRate}. Must be between 0 and 1.);
}
return price * (1 - discountRate);
}
- 使用类型系统
利用类型系统防止误用:
// 使用类型别名增强可读性 type DiscountRate = number; // 0-1 之间 type Price = number; // >= 0
// 更好:使用品牌类型确保类型安全 type DiscountRate = number & { __brand: 'DiscountRate' };
function createDiscountRate(value: number): DiscountRate { if (value < 0 || value > 1) { throw new Error('Discount rate must be between 0 and 1'); } return value as DiscountRate; }
function applyDiscount(price: Price, discountRate: DiscountRate): Price { return (price * (1 - discountRate)) as Price; }
// 使用 const rate = createDiscountRate(0.2); // 类型检查通过 applyDiscount(100, rate);
// applyDiscount(100, 0.2); // ❌ 类型错误!必须使用 createDiscountRate
- 重构前先写测试
如果没有测试,先写测试再重构:
// 步骤 1:为现有(重复的)代码写测试 describe('Original Implementation', () => { it('should calculate order total correctly', () => { const order = { items: [{ price: 10, quantity: 2 }], memberLevel: 'gold' }; expect(calculateOrderTotal(order)).toBe(16); // 20 * 0.8 }); });
// 步骤 2:重构代码
// 步骤 3:确保测试仍然通过 // npm test
检查清单
在完成 DRY 重构后,验证以下内容:
识别阶段 找到了所有明显的代码重复 识别了魔术数字和硬编码字符串 发现了结构性和逻辑性重复 记录了所有重复出现的位置 抽象阶段 清楚区分了可变和不变部分 选择了合适的抽象形式(函数/类/配置) 抽象有清晰、描述性的命名 参数数量合理(≤ 4 个) 没有过度抽象 替换阶段 所有重复点都已替换 没有遗留旧代码 导入和依赖关系正确 处理了所有边缘情况 验证阶段 编写了单元测试 所有现有测试仍然通过 进行了手动验证(如适用) 性能没有显著下降 代码审查已完成(如在团队中工作) 整体质量 代码更易读、易维护 单一职责原则得到遵守 修改只需在一个地方进行 有充分的文档和注释 总结
DRY 原则是软件工程的基石之一。通过系统性地应用这个四步流程,你可以:
识别重复:培养对代码坏味道的敏感度 抽象逻辑:创建可复用、易维护的代码单元 替换实现:消除重复,统一实现 验证测试:确保重构的安全性
记住:
不要过早抽象(等到有明确重复时再抽象) 不要过度抽象(保持简单明了) 小步前进(渐进式重构比一次性大重构更安全) 测试是你的安全网(重构前先写测试)
最终目标:
让每一处知识在系统中都有唯一的、权威的表示。当需要修改时,你只改一个地方,所有使用该知识的地方自动更新。
这就是 DRY 的力量。