mcpField
D
dry-refactoring

dry-refactoring

github.com/yyh211開発28471 回インストール25 週間前に更新
リポジトリ

dry-refactoring について

dry-refactoringは、コーディングエージェントがDRY原則に沿ってコードの重複を見つけ、解消するためのスキルです。識別、抽象化、置換、検証の4段階で進め、テストと過剰な抽象化の回避も扱います。

主な機能

  1. 01構造的・意味的な重複の識別
  2. 02重複ロジックの再利用可能な単位への抽象化
  3. 03重複実装の体系的な置換
  4. 04テストによる動作の検証
  5. 05過剰な抽象化を避けた段階的なリファクタリング

活用シーン

  1. 01コピー&ペーストされたコードの整理
  2. 02重複する定数や設定の集約
  3. 03共通の検証や業務ロジックの抽出
  4. 04リファクタリング後のテストと動作の確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/yyh211/claude-meta-skill --skill dry-refactoring

インストール後、Claude Code で/dry-refactoringを入力して読み込まれたか確認します。

詳細

DRY 標準化プロセス:識別からリファクタリングまで

このスキルでは、4 段階の標準化プロセスを通じて DRY(Don't Repeat Yourself)原則を体系的に適用し、コードの重複を解消して、コードの品質と保守性を高めます。

このスキルを使用する場面

ユーザーから次のような依頼があった場合に、このスキルを使用します:

  • コードの重複や冗長性を解消する
  • 明らかなコピー&ペーストの痕跡があるコードをリファクタリングする
  • DRY 原則を適用してコードベースを最適化する
  • 「コードの不吉な臭い」(マジックナンバーやロジックの重複など)を特定して修正する
  • 共通ロジックを再利用可能な単位として抽出する
  • コードの保守性を高める

主なトリガーワード:DRY、重複コード、コードの重複、リファクタリング、重複の解消、コピー&ペースト、マジックナンバー、コードの不吉な臭い、抽象化、関数の抽出

核心となる考え方

システム内のすべての知識は、単一の明確で信頼できる表現を持たなければなりません。

つまり:

  • ビジネスロジック、アルゴリズム、構成情報は、コードベース内の一か所だけに存在すべきです
  • 変更が必要な場合、変更するのはその一か所だけです
  • そのロジックを使用するすべての場所に、変更が自動的に反映されます

二度目の法則(Rule of Two): ほぼ同じコードブロックを二度目に書いたとき、警鐘が鳴るべきです。これはリファクタリングを始める合図です。

4 段階の標準化プロセス

これは、コーディングのどの段階でも適用できる短いサイクルです。リファクタリングの安全性と有効性を確保するため、手順に厳密に従ってください。


第 1 段階:重複の識別(Identify Repetition)

目標:探偵のようにコードの「不吉な臭い」に注意を払い、重複をすべて見つけます。

1.1 明らかな重複

直接的なコピー&ペースト:

  • 2 つ以上のコードブロックがほぼ同じ見た目
  • 変数名や一部の値が異なるだけ
  • これは最も明白で、最優先で解消すべき重複です

例:

// 重复 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';
  }
}

ロジックの重複:

  • コードの見た目が異なる 2 つの関数がある
  • しかし、ビジネスロジックの観点では同じ目的を実現している

例:

// 逻辑重复:都在计算折扣,只是来源不同
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 構造
  • 機能は似ているが、名前が異なる関数
  • 同じアルゴリズムが別の場所で再実装されている
  • 同じ検証ロジックが複数の場所に分散している

💡 アクション:検索機能を使い、重複するリテラルや似た関数名のパターンを探します。重複が見つかったすべての場所を記録してください。


第 2 段階:ロジックの抽象化(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):

  • ❌ 汎用性が高すぎる抽象化(「万能関数」)を作る
  • ❌ 早まって抽象化する(使用箇所が 1 つだけなら抽象化しない)
  • ❌ パフォーマンスへの影響を無視する(不要な関数呼び出しのオーバーヘッドなど)
  • ❌ 理解しにくい抽象化を使う(認知負荷を増やす)

💡 アクション:新しい関数、クラス、または構成ファイルを作成し、「不変部分」をその中に入れ、「可変部分」を引数として定義します。


第 3 段階:実装の置き換え(Replace the Implementation)

目標:古い重複コードをすべて、新しい抽象化単位に置き換えます。

3.1 系統的な置き換え

手順:

  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
置き換えチェックリスト
  • すべての重複箇所が置き換えられていることを確認する(見落とさない)
  • 古い重複コードを削除する(新旧の方法を混在させない)
  • インポート文と依存関係を確認する
  • 引数の順序と型が正しいことを確認する
  • すべてのエッジケースに対応する

⚠️ 警告:一部だけを置き換えると、別の不整合が生まれ、状況がさらに悪化する可能性があります。すべて置き換えるか、まったく置き換えないようにしてください。

💡 アクション:IDE の「すべての参照を検索」機能を使い、重複箇所を見落としていないことを確認します。


第 4 段階:検証とテスト(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 コードレビュー

チームで作業している場合は、コードレビューを実施します:

レビューの確認項目:

  • 抽象化は適切で、理解しやすいか?
  • 命名は明確で、規約に沿っているか?
  • 見落とした重複箇所はないか?
  • 抽象化や設計が過剰に複雑ではないか?
  • ドキュメントやコメントは十分か?

💡 アクション:関連するすべてのテストを実行し、プログラムの外部動作がリファクタリングの前後で完全に一致することを確認します。テストがありませんか?今がテストを書く絶好の機会です。


完全な例:最初から最後まで

シナリオ:EC システムの割引計算
元のコード(重複あり)
// 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.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<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 的力量。

コメント