dry-refactoring

SKILL.md

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 系统性替换

步骤

  1. 定位所有重复点:回到第一步记录的所有位置
  2. 逐一替换:删除旧代码,调用新抽象
  3. 传入正确参数:确保参数对应正确
  4. 保持行为一致:确保替换前后功能完全相同

替换示例

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 单元测试

为你新创建的抽象编写独立的单元测试:

测试覆盖要点

  • ✅ 正常输入的正确输出
  • ✅ 边界值测试(空值、最大值、最小值)
  • ✅ 异常输入的错误处理
  • ✅ 不同参数组合的行为

示例

# 抽象函数
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:识别重复

发现的重复

  1. 计算商品总价的循环逻辑(结构重复)
  2. 折扣计算逻辑(逻辑重复)
  3. 魔术数字 0.80.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<string, User>;  // 直接暴露内部数据结构

  getUser(id: string) {
    return this.users.get(id);
  }
}

// ✅ 隐藏内部实现
class UserManager {
  private users: Map<string, User>;

  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. 继续其他模块...

2. 文档化你的抽象

好的抽象需要好的文档:

/**
 * 计算商品折扣后的价格
 *
 * @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);
}

3. 使用类型系统

利用类型系统防止误用:

// 使用类型别名增强可读性
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

4. 重构前先写测试

如果没有测试,先写测试再重构:

// 步骤 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 原则是软件工程的基石之一。通过系统性地应用这个四步流程,你可以:

  1. 识别重复:培养对代码坏味道的敏感度
  2. 抽象逻辑:创建可复用、易维护的代码单元
  3. 替换实现:消除重复,统一实现
  4. 验证测试:确保重构的安全性

记住

  • 不要过早抽象(等到有明确重复时再抽象)
  • 不要过度抽象(保持简单明了)
  • 小步前进(渐进式重构比一次性大重构更安全)
  • 测试是你的安全网(重构前先写测试)

最终目标

让每一处知识在系统中都有唯一的、权威的表示。当需要修改时,你只改一个地方,所有使用该知识的地方自动更新。

这就是 DRY 的力量。

Weekly Installs
36
GitHub Stars
231
First Seen
Jan 21, 2026
Installed on
opencode30
gemini-cli29
codex27
github-copilot27
cursor23
amp21