BMAD-METHOD/expansion-packs/tdd-methodology/templates/tdd-refactor.md

563 lines
13 KiB
Markdown

<!-- Powered by BMAD™ Core -->
# TDD Refactor Phase Prompts
Instructions for Dev and QA agents when refactoring code while maintaining green tests in Test-Driven Development.
## Core Refactor Phase Mindset
**You are in TDD REFACTOR PHASE. Your mission is to improve code quality while keeping ALL tests green. Every change must preserve existing behavior.**
### Primary Objectives
1. **Preserve behavior** - External behavior must remain exactly the same
2. **Improve design** - Make code more readable, maintainable, and extensible
3. **Eliminate technical debt** - Remove duplication, improve naming, fix code smells
4. **Maintain test coverage** - All tests must stay green throughout
5. **Small steps** - Make incremental improvements with frequent test runs
## Refactoring Safety Rules
### The Golden Rule
**NEVER proceed with a refactoring step if tests are red.** Always revert and try smaller changes.
### Safe Refactoring Workflow
```yaml
refactoring_cycle:
1. identify_smell: 'Find specific code smell to address'
2. plan_change: 'Decide on minimal improvement step'
3. run_tests: 'Ensure all tests are green before starting'
4. make_change: 'Apply single, small refactoring'
5. run_tests: 'Verify tests are still green'
6. commit: 'Save progress if tests pass'
7. repeat: 'Move to next improvement'
abort_conditions:
- tests_turn_red: 'Immediately revert and try smaller step'
- behavior_changes: 'Revert if external interface changes'
- complexity_increases: 'Revert if code becomes harder to understand'
```
## Code Smells and Refactoring Techniques
### Duplication Elimination
**Before: Repeated validation logic**
```javascript
function createUser(data) {
if (!data.email.includes('@')) {
throw new Error('Invalid email format');
}
return { id: generateId(), ...data };
}
function updateUser(id, data) {
if (!data.email.includes('@')) {
throw new Error('Invalid email format');
}
return { id, ...data };
}
```
**After: Extract validation function**
```javascript
function validateEmail(email) {
if (!email.includes('@')) {
throw new Error('Invalid email format');
}
}
function createUser(data) {
validateEmail(data.email);
return { id: generateId(), ...data };
}
function updateUser(id, data) {
validateEmail(data.email);
return { id, ...data };
}
```
### Long Method Refactoring
**Before: Method doing too much**
```javascript
function processUserRegistration(userData) {
// Validation (5 lines)
if (!userData.email.includes('@')) throw new Error('Invalid email');
if (!userData.name || userData.name.trim().length === 0) throw new Error('Name required');
if (userData.age < 18) throw new Error('Must be 18 or older');
// Data transformation (4 lines)
const user = {
id: generateId(),
email: userData.email.toLowerCase(),
name: userData.name.trim(),
age: userData.age,
};
// Business logic (3 lines)
if (userData.age >= 65) {
user.discountEligible = true;
}
return user;
}
```
**After: Extract methods**
```javascript
function validateUserData(userData) {
if (!userData.email.includes('@')) throw new Error('Invalid email');
if (!userData.name || userData.name.trim().length === 0) throw new Error('Name required');
if (userData.age < 18) throw new Error('Must be 18 or older');
}
function normalizeUserData(userData) {
return {
id: generateId(),
email: userData.email.toLowerCase(),
name: userData.name.trim(),
age: userData.age,
};
}
function applyBusinessRules(user) {
if (user.age >= 65) {
user.discountEligible = true;
}
return user;
}
function processUserRegistration(userData) {
validateUserData(userData);
const user = normalizeUserData(userData);
return applyBusinessRules(user);
}
```
### Magic Numbers and Constants
**Before: Magic numbers scattered**
```javascript
function calculateShipping(weight) {
if (weight < 5) {
return 4.99;
} else if (weight < 20) {
return 9.99;
} else {
return 19.99;
}
}
```
**After: Named constants**
```javascript
const SHIPPING_RATES = {
LIGHT_WEIGHT_THRESHOLD: 5,
MEDIUM_WEIGHT_THRESHOLD: 20,
LIGHT_SHIPPING_COST: 4.99,
MEDIUM_SHIPPING_COST: 9.99,
HEAVY_SHIPPING_COST: 19.99,
};
function calculateShipping(weight) {
if (weight < SHIPPING_RATES.LIGHT_WEIGHT_THRESHOLD) {
return SHIPPING_RATES.LIGHT_SHIPPING_COST;
} else if (weight < SHIPPING_RATES.MEDIUM_WEIGHT_THRESHOLD) {
return SHIPPING_RATES.MEDIUM_SHIPPING_COST;
} else {
return SHIPPING_RATES.HEAVY_SHIPPING_COST;
}
}
```
### Variable Naming Improvements
**Before: Unclear names**
```javascript
function calc(u, p) {
const t = u * p;
const d = t * 0.1;
return t - d;
}
```
**After: Intention-revealing names**
```javascript
function calculateNetPrice(unitPrice, quantity) {
const totalPrice = unitPrice * quantity;
const discount = totalPrice * 0.1;
return totalPrice - discount;
}
```
## Refactoring Strategies by Code Smell
### Complex Conditionals
**Before: Nested conditions**
```javascript
function determineUserType(user) {
if (user.age >= 18) {
if (user.hasAccount) {
if (user.isPremium) {
return 'premium-member';
} else {
return 'basic-member';
}
} else {
return 'guest-adult';
}
} else {
return 'minor';
}
}
```
**After: Guard clauses and early returns**
```javascript
function determineUserType(user) {
if (user.age < 18) {
return 'minor';
}
if (!user.hasAccount) {
return 'guest-adult';
}
return user.isPremium ? 'premium-member' : 'basic-member';
}
```
### Large Classes (God Object)
**Before: Class doing too much**
```javascript
class UserManager {
validateUser(data) {
/* validation logic */
}
createUser(data) {
/* creation logic */
}
sendWelcomeEmail(user) {
/* email logic */
}
logUserActivity(user, action) {
/* logging logic */
}
calculateUserStats(user) {
/* analytics logic */
}
}
```
**After: Single responsibility classes**
```javascript
class UserValidator {
validate(data) {
/* validation logic */
}
}
class UserService {
create(data) {
/* creation logic */
}
}
class EmailService {
sendWelcome(user) {
/* email logic */
}
}
class ActivityLogger {
log(user, action) {
/* logging logic */
}
}
class UserAnalytics {
calculateStats(user) {
/* analytics logic */
}
}
```
## Collaborative Refactoring (Dev + QA)
### When to Involve QA Agent
**QA Agent should participate when:**
```yaml
qa_involvement_triggers:
test_modification_needed:
- 'Test expectations need updating'
- 'New test cases discovered during refactoring'
- 'Mock strategies need adjustment'
coverage_assessment:
- 'Refactoring exposes untested code paths'
- 'New methods need test coverage'
- 'Test organization needs improvement'
design_validation:
- 'Interface changes affect test structure'
- 'Mocking strategy becomes complex'
- 'Test maintainability concerns'
```
### Dev-QA Collaboration Workflow
```yaml
collaborative_steps:
1. dev_identifies_refactoring: 'Dev spots code smell'
2. assess_test_impact: 'Both agents review test implications'
3. plan_refactoring: 'Agree on approach and steps'
4. dev_refactors: 'Dev makes incremental changes'
5. qa_validates_tests: 'QA ensures tests remain valid'
6. both_review: 'Joint review of improved code and tests'
```
## Advanced Refactoring Patterns
### Extract Interface for Testability
**Before: Hard to test due to dependencies**
```javascript
class OrderService {
constructor() {
this.emailSender = new EmailSender();
this.paymentProcessor = new PaymentProcessor();
}
processOrder(order) {
const result = this.paymentProcessor.charge(order.total);
this.emailSender.sendConfirmation(order.customerEmail);
return result;
}
}
```
**After: Dependency injection for testability**
```javascript
class OrderService {
constructor(emailSender, paymentProcessor) {
this.emailSender = emailSender;
this.paymentProcessor = paymentProcessor;
}
processOrder(order) {
const result = this.paymentProcessor.charge(order.total);
this.emailSender.sendConfirmation(order.customerEmail);
return result;
}
}
// Usage in production:
const orderService = new OrderService(new EmailSender(), new PaymentProcessor());
// Usage in tests:
const mockEmail = { sendConfirmation: jest.fn() };
const mockPayment = { charge: jest.fn().mockReturnValue('success') };
const orderService = new OrderService(mockEmail, mockPayment);
```
### Replace Conditional with Polymorphism
**Before: Switch statement**
```javascript
function calculateArea(shape) {
switch (shape.type) {
case 'circle':
return Math.PI * shape.radius * shape.radius;
case 'rectangle':
return shape.width * shape.height;
case 'triangle':
return 0.5 * shape.base * shape.height;
default:
throw new Error('Unknown shape type');
}
}
```
**After: Polymorphic classes**
```javascript
class Circle {
constructor(radius) {
this.radius = radius;
}
calculateArea() {
return Math.PI * this.radius * this.radius;
}
}
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
calculateArea() {
return this.width * this.height;
}
}
class Triangle {
constructor(base, height) {
this.base = base;
this.height = height;
}
calculateArea() {
return 0.5 * this.base * this.height;
}
}
```
## Refactoring Safety Checks
### Before Each Refactoring Step
```bash
# 1. Ensure all tests are green
npm test
pytest
go test ./...
# 2. Consider impact
# - Will this change external interfaces?
# - Are there hidden dependencies?
# - Could this affect performance significantly?
# 3. Plan the smallest possible step
# - What's the minimal change that improves code?
# - Can this be broken into smaller steps?
```
### After Each Refactoring Step
```bash
# 1. Run tests immediately
npm test
# 2. If tests fail:
git checkout -- . # Revert changes
# Plan smaller refactoring step
# 3. If tests pass:
git add .
git commit -m "REFACTOR: Extract validateEmail function [maintains UC-001, UC-002]"
```
## Refactoring Anti-Patterns
### Don't Change Behavior
```javascript
// Wrong: Changing logic during refactoring
function calculateDiscount(amount) {
// Original: 10% discount
return amount * 0.1;
// Refactored: DON'T change the discount rate
return amount * 0.15; // This changes behavior!
}
// Right: Only improve structure
const DISCOUNT_RATE = 0.1; // Extract constant
function calculateDiscount(amount) {
return amount * DISCOUNT_RATE; // Same behavior
}
```
### Don't Add Features
```javascript
// Wrong: Adding features during refactoring
function validateUser(userData) {
validateEmail(userData.email); // Existing
validateName(userData.name); // Existing
validateAge(userData.age); // DON'T add new validation
}
// Right: Only improve existing code
function validateUser(userData) {
validateEmail(userData.email);
validateName(userData.name);
// Age validation needs its own failing test first
}
```
### Don't Make Large Changes
```javascript
// Wrong: Massive refactoring in one step
class UserService {
// Completely rewrite entire class structure
}
// Right: Small, incremental improvements
class UserService {
// Extract one method at a time
// Rename one variable at a time
// Improve one code smell at a time
}
```
## Refactor Phase Checklist
Before considering refactoring complete:
- [ ] **All tests remain green** - No test failures introduced
- [ ] **Code quality improved** - Measurable improvement in readability/maintainability
- [ ] **No behavior changes** - External behavior is identical
- [ ] **Technical debt reduced** - Specific code smells addressed
- [ ] **Small commits made** - Each improvement committed separately
- [ ] **Documentation updated** - Comments and docs reflect changes
- [ ] **Performance maintained** - No significant performance degradation
- [ ] **Story metadata updated** - Refactoring notes and improvements documented
## Success Indicators
**Refactoring is successful when:**
1. **All tests consistently pass** throughout the process
2. **Code is noticeably easier to read** and understand
3. **Duplication has been eliminated** or significantly reduced
4. **Method/class sizes are more reasonable** (functions < 15 lines)
5. **Variable and function names clearly express intent**
6. **Code complexity has decreased** (fewer nested conditions)
7. **Future changes will be easier** due to better structure
**Refactoring is complete when:**
- No obvious code smells remain in the story scope
- Code quality metrics show improvement
- Tests provide comprehensive safety net
- Ready for next TDD cycle or story completion
Remember: Refactoring is about improving design, not adding features. Keep tests green, make small changes, and focus on making the code better for the next developer!