AgentSkillsCN

refactoring

重构模式——在不改变程序行为的前提下,持续优化代码设计

SKILL.md
--- frontmatter
name: refactoring
description: Refactoring patterns - improving code design without changing behavior

/refactoring — Refactoring Patterns

Channel Martin Fowler, Michael Feathers, and Joshua Kerievsky.

Core Philosophy

"Refactoring is a disciplined technique for restructuring an existing body of code, altering its internal structure without changing its external behavior." — Fowler

The Two Hats:

  • Adding functionality (don't change existing code)
  • Refactoring (don't add functionality)

Never wear both hats at once.

Smell-driven, not pattern-driven. Identify a smell first. Pick the minimum refactoring to fix it. Never scan for opportunities to introduce patterns.

Code Smells → Refactorings (Fowler)

Bloaters

SmellRefactoring
Long MethodExtract Method, Replace Temp with Query
Large ClassExtract Class, Extract Subclass
Long Parameter ListIntroduce Parameter Object, Preserve Whole Object
Data ClumpsExtract Class, Introduce Parameter Object
Primitive ObsessionReplace Primitive with Object, Replace Type Code with Class

Object-Orientation Abusers

SmellRefactoring
Switch StatementsReplace Conditional with Polymorphism
Parallel InheritanceMove Method, Move Field
Lazy ClassInline Class, Collapse Hierarchy
Speculative GeneralityCollapse Hierarchy, Inline Class, Remove Parameter
Temporary FieldExtract Class, Introduce Null Object

Change Preventers

SmellRefactoring
Divergent ChangeExtract Class
Shotgun SurgeryMove Method, Move Field, Inline Class
Parallel InheritanceMove Method, Move Field

Dispensables

SmellRefactoring
Comments (as deodorant)Extract Method, Rename Method
Duplicate CodeExtract Method, Pull Up Method, Form Template Method
Dead CodeRemove Dead Code
Lazy ClassInline Class
Speculative GeneralityCollapse Hierarchy, Remove Parameter

Couplers

SmellRefactoring
Feature EnvyMove Method, Extract Method
Inappropriate IntimacyMove Method, Move Field, Hide Delegate
Message ChainsHide Delegate, Extract Method, Move Method
Middle ManRemove Middle Man, Inline Method

Smell → Pattern Mappings (Kerievsky)

Apply patterns only when a smell justifies the destination. The smell is the trigger — the pattern is the minimum fix.

SmellRefactoring to PatternJustification Gate
Repeated conditional on typeReplace Conditional with Strategy3+ branches on same discriminator
Constructor with many combosReplace Constructors with Builder4+ optional parameters, callers use different subsets
Notification spaghettiReplace Hard-Coded Notifications with Observer3+ listeners or listeners change at runtime
Embedded algorithm variesReplace Algorithm with Strategy2+ variants exist or are imminent
Composite structure with type checksReplace Implicit Tree with CompositeRecursive structure with uniform operations
Accumulating decorationsReplace Layered Behavior with DecoratorBehaviors compose independently
State-dependent conditionalsReplace State-Altering Conditionals with State3+ states with transition logic spread across methods
Complex object creationReplace Constructor with Factory MethodCreation varies by context or subtype
<never> Do NOT apply these patterns speculatively. Each row requires the smell to be present AND the justification gate to be met. A single switch statement is not enough for Strategy. Two constructor parameters is not enough for Builder. </never>

Working with Untested Code (Feathers)

When code has no tests, you cannot safely refactor. Use these techniques to get tests in place first.

Characterization Tests

Write tests that document current behavior, not intended behavior:

  1. Write a test you expect to fail
  2. Run it — observe the actual output
  3. Change the test to assert the actual output
  4. You now have a characterization test that locks existing behavior

Characterization tests are not aspirational. They describe what the code does, not what it should do.

Finding Seams

A seam is a place where you can alter behavior without editing the source. Three types:

Seam TypeHow It WorksWhen to Use
Object seamOverride method in subclass or pass different implementationClass with injectable dependency
Link seamSwap module/import at build or test timeModule-level dependency
Preprocessing seamConditional compilation or feature flagsBuild-time variation

Dependency-Breaking Techniques

Use these to get legacy code under test:

TechniqueWhat It Does
Extract InterfaceCreate interface from class to enable substitution
Parameterize ConstructorPass dependency in instead of creating internally
Subclass and Override MethodOverride the problematic method in a test subclass
Extract and Override CallMove a hard-to-test call into its own method, override in test
Replace Global Reference with GetterWrap global access in a method you can override
Introduce Instance DelegatorReplace static method with instance method that delegates to it

The Legacy Code Dilemma

To refactor safely, you need tests. To add tests, you often need to refactor. Break the deadlock:

  1. Identify the change point — where you need to make your change
  2. Find the seams — places to inject test behavior
  3. Break one dependency using the simplest technique above
  4. Write characterization tests around the change point
  5. Now refactor safely

Key Refactorings (Mechanics)

Extract Method

javascript
// Before
function printOwing() {
  printBanner();
  // print details
  console.log("name: " + name);
  console.log("amount: " + getOutstanding());
}

// After
function printOwing() {
  printBanner();
  printDetails(getOutstanding());
}

function printDetails(outstanding) {
  console.log("name: " + name);
  console.log("amount: " + outstanding);
}

Replace Conditional with Polymorphism

javascript
// Before
function getSpeed() {
  switch (this.type) {
    case 'european': return getBaseSpeed();
    case 'african': return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
    case 'norwegian_blue': return isNailed ? 0 : getBaseSpeed();
  }
}

// After
class European extends Bird {
  getSpeed() { return this.getBaseSpeed(); }
}
class African extends Bird {
  getSpeed() { return this.getBaseSpeed() - this.getLoadFactor() * this.numberOfCoconuts; }
}
class NorwegianBlue extends Bird {
  getSpeed() { return this.isNailed ? 0 : this.getBaseSpeed(); }
}

Introduce Parameter Object

javascript
// Before
function amountInvoiced(startDate, endDate) { ... }
function amountReceived(startDate, endDate) { ... }
function amountOverdue(startDate, endDate) { ... }

// After
class DateRange {
  constructor(start, end) { this.start = start; this.end = end; }
}
function amountInvoiced(dateRange) { ... }
function amountReceived(dateRange) { ... }
function amountOverdue(dateRange) { ... }

Slide Statements (Fowler, 2nd ed.)

javascript
// Before — related code is scattered
const price = order.basePrice;
sendConfirmation(order);
const discount = calculateDiscount(price);

// After — related code is together
const price = order.basePrice;
const discount = calculateDiscount(price);
sendConfirmation(order);

Replace Loop with Pipeline (Fowler, 2nd ed.)

javascript
// Before
const result = [];
for (const person of people) {
  if (person.age > 18) {
    result.push(person.name);
  }
}

// After
const result = people
  .filter(p => p.age > 18)
  .map(p => p.name);

Split Phase

javascript
// Before — parsing and calculation interleaved
function priceOrder(product, quantity, shippingMethod) {
  const basePrice = product.basePrice * quantity;
  const discount = Math.max(quantity - 500, 0) * product.basePrice * 0.05;
  const shippingCost = calcShipping();
  return basePrice - discount + shippingCost;
}

// After — separated into pricing phase and shipping phase
function priceOrder(product, quantity, shippingMethod) {
  const priceData = calculatePricingData(product, quantity);
  return applyShipping(priceData, shippingMethod);
}

The Refactoring Process

  1. Ensure tests pass before starting
  2. If no tests exist, write characterization tests first (Feathers)
  3. Make small changes — one refactoring at a time
  4. Run tests after each change
  5. Commit frequently — each refactoring is a commit
  6. If tests fail, revert immediately

When to Refactor

  • Rule of Three: First time, just do it. Second time, wince. Third time, refactor.
  • Before adding a feature: Make the code easier to add the feature first (preparatory refactoring)
  • During code review: Spot smells, suggest refactorings
  • When debugging: If code is hard to understand, refactor first (comprehension refactoring)

When NOT to Refactor

  • Code is broken (fix bugs first)
  • Deadline is imminent (but schedule refactoring after)
  • Complete rewrite is needed
  • No tests exist and you can't find seams (write characterization tests first)

Concrete Checks

  • Smell identified? Name the specific Fowler smell before choosing a refactoring
  • Minimum fix? Is this the smallest refactoring that addresses the smell?
  • Tests green before? Did you run tests before touching anything?
  • Tests green after? Did you run tests after each individual change?
  • Pattern justified? If introducing a GoF pattern, does the Kerievsky justification gate pass?
  • One hat? Are you only refactoring, or are you sneaking in behavior changes?

References

  • "Refactoring: Improving the Design of Existing Code" (2nd ed.) — Martin Fowler
  • "Working Effectively with Legacy Code" — Michael Feathers
  • "Refactoring to Patterns" — Joshua Kerievsky
  • refactoring.com — Online catalog of refactorings