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<T> {
private store = new Map<string, T>();
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<User>();
const productCache = new Cache<Product>();
抽象设计原则
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<T>(
data: T,
customStep: (item: T) => T
): T {
// 前置处理
const prepared = prepare(data);
// 可变的自定义步骤
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 单元测试
为你新创建的抽象编写独立的单元测试:
测试覆盖要点:
- ✅ 正常输入的正确输出
- ✅ 边界值测试(空值、最大