
Code Refactoring
- 27 installs
- 14 repo stars
- Updated January 23, 2026
- dauquangthanh/hanoi-rainbow
Code Refactoring is an agent skill that identifies code smells and applies incremental refactoring patterns so developers can improve design without breaking behavior.
About
Code Refactoring is a Hanoi Rainbow skill that enforces a safe, test-first refactoring loop with named patterns for smells like long methods and duplicated logic. Use it when cleaning up legacy modules, reducing complexity before new features, or responding to quality-review findings. It preserves functionality through incremental steps rather than sweeping rewrites without coverage.
- Safe workflow: tests before each change
- Catalog of extract, move, and rename patterns
- Code smell identification checklist
- Incremental commit-friendly steps
Code Refactoring by the numbers
- 27 all-time installs (skills.sh)
- Ranked #684 of 1,352 Code Review & Quality skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dauquangthanh/hanoi-rainbow --skill code-refactoringAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 27 |
|---|---|
| repo stars | ★ 14 |
| Last updated | January 23, 2026 |
| Repository | dauquangthanh/hanoi-rainbow ↗ |
How do you improve messy or debt-laden code safely without introducing regressions?
Guides safe, test-backed refactoring with smell identification and incremental pattern application.
Who is it for?
Developers with existing or newly added tests who need guided, incremental refactors on backend or shared logic.
Skip if: Greenfield projects with no code to refactor, or teams unwilling to add tests before structural changes.
When should I use this skill?
You are addressing code smells, technical debt, or pre-feature cleanup and need a step-by-step refactoring workflow.
What you get
Cleaner structure through applied patterns, with tests run after each incremental change and documented technique choices.
Files
Code Refactoring
Overview
This skill guides systematic code refactoring to improve code quality, maintainability, and design while preserving functionality. Follow the safe refactoring workflow with comprehensive test coverage and incremental changes.
Refactoring Workflow
Step 1: Analyze Code and Identify Issues
Examine the codebase to identify code smells and quality issues:
- Long methods (>20-30 lines) or large classes (>300-500 lines)
- Duplicated code blocks or similar logic in multiple places
- Unclear or misleading names for variables, methods, or classes
- Complex conditional logic or deeply nested structures
- Poor separation of concerns or tight coupling between components
For detailed code smell catalog: See code-smells.md
Step 2: Verify Test Coverage
Before refactoring ANY code:
1. Check existing test coverage for the code to be refactored 2. If tests are missing or inadequate, write tests FIRST 3. Run all tests to establish baseline (all should pass) 4. Never proceed without adequate test coverage
For test coverage strategies: See testing-strategies.md
Step 3: Choose Refactoring Technique
Select the appropriate refactoring pattern based on the issue:
- Extract Method/Function: Break down long methods into smaller, focused ones
- Extract Class: Split large classes with multiple responsibilities
- Rename: Improve clarity with better names
- Move Method/Field: Relocate functionality to more appropriate classes
- Replace Conditional with Polymorphism: Simplify complex conditionals
- Introduce Parameter Object: Group related parameters
- Inline Method/Variable: Remove unnecessary indirection
For complete pattern catalog: See refactoring-patterns.md
Step 4: Apply Refactoring Incrementally
Make ONE small change at a time:
1. Apply a single refactoring technique 2. Run all tests immediately after the change 3. If tests pass, commit the change 4. If tests fail, revert and try a different approach 5. Repeat for each refactoring needed
Critical Rules:
- Never change behavior while refactoring
- Never refactor and add features simultaneously
- Use IDE automated refactoring tools when available
- Keep each refactoring commit small and focused
For detailed process guidance: See refactoring-process.md
Step 5: Verify and Document
After completing refactorings:
1. Run full test suite to ensure all tests pass 2. Check that code quality metrics improved 3. Review code to confirm readability enhanced 4. Document significant architectural changes if needed 5. Create clear commit messages describing refactorings
Common Refactoring Scenarios
Scenario-specific guidance is available for:
- Legacy code modernization
- Preparing code for new features
- Performance optimization through refactoring
- Reducing technical debt systematically
- Extracting reusable components
See common-refactoring-scenarios.md for detailed examples and approaches.
Best Practices and Quality Guidelines
Follow established principles for high-quality refactoring:
- Apply SOLID principles (Single Responsibility, Open/Closed, etc.)
- Reduce coupling between components
- Increase cohesion within components
- Eliminate duplication (DRY principle)
- Maintain consistent coding standards
For comprehensive best practices: See refactoring-best-practices.md
Tools and Automation
Modern IDEs and tools can automate many refactorings safely:
- IDE refactoring features (IntelliJ, VS Code, Visual Studio)
- Static analysis tools for code smell detection
- Test coverage tools
- Automated code formatting and linting
For tool recommendations and usage: See tools-and-automation.md
Output Format
When presenting refactoring recommendations:
1. Identify the code smell or quality issue 2. Explain why it's problematic 3. Propose specific refactoring approach 4. Show before/after code examples 5. List tests to verify behavior preservation
For detailed output templates: See output-format.md
Code Smells Identification Guide
This reference provides comprehensive guidance on identifying and addressing code smells - indicators of deeper problems in code design.
Bloaters
Code that has grown so large and unwieldy that it's difficult to work with.
Long Method
Symptoms: Method contains too many lines of code (generally >20-30 lines)
Problems:
- Hard to understand
- Difficult to test
- Likely violates Single Responsibility Principle
- Contains hidden duplication
Detection:
// Red flags:
// - Method spans multiple screens
// - Deeply nested conditionals
// - Multiple levels of abstraction
// - Lots of local variables
// - Comments explaining sections
async function processOrder(orderId: string) {
// Validate order - 20 lines
const order = await db.orders.findById(orderId);
if (!order) throw new Error('Order not found');
if (order.status !== 'pending') throw new Error('Invalid status');
// ... 15 more validation lines
// Calculate totals - 30 lines
let subtotal = 0;
for (const item of order.items) {
const product = await db.products.findById(item.productId);
// ... 25 more calculation lines
}
// Apply discounts - 25 lines
if (order.couponCode) {
// ... 20 discount calculation lines
}
// Process payment - 30 lines
const payment = await paymentGateway.charge({...});
// ... 25 more payment lines
// Update inventory - 20 lines
for (const item of order.items) {
// ... 15 inventory update lines
}
// Send notifications - 15 lines
await emailService.send({...});
// ... 10 notification lines
}Solutions:
1. Extract Method 2. Replace Temp with Query 3. Introduce Parameter Object 4. Preserve Whole Object 5. Replace Method with Method Object
// Fixed version
async function processOrder(orderId: string) {
const order = await validateAndGetOrder(orderId);
const total = await calculateTotal(order);
const payment = await processPayment(order.customerId, total);
await updateInventory(order);
await sendConfirmation(order);
return completeOrder(order, payment);
}Large Class
Symptoms: Class has many fields, methods, or lines of code (>300 lines)
Problems:
- Too many responsibilities
- Hard to understand and maintain
- Difficult to test
- High coupling
Detection:
// Red flags:
// - More than 10 fields
// - More than 20 methods
// - Multiple unrelated responsibilities
// - Fields used only by subset of methods
class UserManager {
// User data
id: string;
email: string;
password: string;
name: string;
avatar: string;
// Authentication
async login(password: string) { }
async logout() { }
async refreshToken() { }
async resetPassword() { }
// Profile management
updateProfile(data: any) { }
uploadAvatar(file: File) { }
deleteAvatar() { }
// Email operations
sendWelcomeEmail() { }
sendPasswordResetEmail() { }
sendNotificationEmail() { }
// Preferences
updateEmailPreferences() { }
updatePrivacySettings() { }
updateNotificationSettings() { }
// Statistics
getLoginCount() { }
getLastLoginDate() { }
getActivitySummary() { }
// Friends/Social
addFriend(friendId: string) { }
removeFriend(friendId: string) { }
getFriendsList() { }
// Payment
addPaymentMethod() { }
removePaymentMethod() { }
getPaymentHistory() { }
}Solutions:
1. Extract Class 2. Extract Subclass 3. Extract Interface 4. Duplicate Observed Data
// Fixed: Separated into focused classes
class User {
constructor(
public id: string,
public email: string,
public name: string
) {}
}
class UserAuthentication {
async login(user: User, password: string): Promise<Token> { }
async logout(token: Token): Promise<void> { }
async refreshToken(token: Token): Promise<Token> { }
}
class UserProfile {
async update(userId: string, data: ProfileData): Promise<void> { }
async uploadAvatar(userId: string, file: File): Promise<string> { }
}
class UserNotifications {
async sendWelcome(user: User): Promise<void> { }
async sendPasswordReset(user: User): Promise<void> { }
}
class UserPreferences {
async updateEmail(userId: string, prefs: EmailPrefs): Promise<void> { }
async updatePrivacy(userId: string, prefs: PrivacyPrefs): Promise<void> { }
}
class UserStatistics {
async getLoginCount(userId: string): Promise<number> { }
async getActivitySummary(userId: string): Promise<Activity> { }
}Primitive Obsession
Symptoms: Using primitives instead of small objects for simple tasks
Problems:
- Validation logic scattered
- Type safety issues
- Harder to extend behavior
- No encapsulation
Detection:
// Red flags:
// - String/number used for domain concepts
// - Validation repeated across codebase
// - Magic numbers/strings
// - No type safety for domain concepts
class User {
email: string; // Just a string, no validation
phoneNumber: string; // Could be any format
zipCode: string; // No validation
country: string; // Could be "USA", "US", "United States"
status: number; // 0, 1, 2 - what do they mean?
constructor(
email: string,
phoneNumber: string,
zipCode: string,
country: string,
status: number
) {
// Validation scattered here
if (!email.includes('@')) throw new Error('Invalid email');
if (phoneNumber.length < 10) throw new Error('Invalid phone');
// ...
}
}
// Usage problems
const user = new User(
'invalid-email', // No compile-time checking
'123', // Wrong format
'ABC', // Invalid zip
'United States', // Inconsistent
5 // What does 5 mean?
);Solutions:
1. Replace Data Value with Object 2. Replace Type Code with Class 3. Extract Class 4. Introduce Parameter Object
// Fixed: Value objects with validation
class Email {
private constructor(private readonly value: string) {}
static create(email: string): Email {
if (!email || !email.includes('@')) {
throw new ValidationError('Invalid email format');
}
return new Email(email.toLowerCase());
}
getValue(): string {
return this.value;
}
equals(other: Email): boolean {
return this.value === other.value;
}
}
class PhoneNumber {
private constructor(private readonly value: string) {}
static create(phone: string): PhoneNumber {
const cleaned = phone.replace(/\D/g, '');
if (cleaned.length !== 10) {
throw new ValidationError('Phone must be 10 digits');
}
return new PhoneNumber(cleaned);
}
getValue(): string {
return this.value;
}
format(): string {
return `(${this.value.slice(0, 3)}) ${this.value.slice(3, 6)}-${this.value.slice(6)}`;
}
}
class ZipCode {
private constructor(private readonly value: string) {}
static create(zip: string): ZipCode {
if (!/^\d{5}(-\d{4})?$/.test(zip)) {
throw new ValidationError('Invalid ZIP code format');
}
return new ZipCode(zip);
}
getValue(): string {
return this.value;
}
}
enum Country {
US = 'US',
CA = 'CA',
UK = 'UK'
}
enum UserStatus {
ACTIVE = 'ACTIVE',
INACTIVE = 'INACTIVE',
SUSPENDED = 'SUSPENDED'
}
class User {
constructor(
public email: Email,
public phoneNumber: PhoneNumber,
public zipCode: ZipCode,
public country: Country,
public status: UserStatus
) {}
}
// Usage: Compile-time safety
const user = new User(
Email.create('user@example.com'),
PhoneNumber.create('555-123-4567'),
ZipCode.create('12345'),
Country.US,
UserStatus.ACTIVE
);Long Parameter List
Symptoms: Method has more than 3-4 parameters
Problems:
- Hard to understand
- Easy to pass parameters in wrong order
- Difficult to extend
- Creates dependencies
Detection:
// Red flags:
// - More than 3-4 parameters
// - Parameters often changed together
// - Parameters come from same object
function createUser(
email: string,
password: string,
firstName: string,
lastName: string,
age: number,
country: string,
city: string,
street: string,
zipCode: string,
phoneNumber: string
) {
// Too many parameters!
}Solutions:
1. Replace Parameter with Method Call 2. Preserve Whole Object 3. Introduce Parameter Object
// Solution 1: Parameter object
interface CreateUserParams {
credentials: {
email: string;
password: string;
};
profile: {
firstName: string;
lastName: string;
age: number;
};
address: {
country: string;
city: string;
street: string;
zipCode: string;
};
contact: {
phoneNumber: string;
};
}
function createUser(params: CreateUserParams) {
// Much clearer!
}
// Solution 2: Builder pattern
class UserBuilder {
private user: Partial<User> = {};
withCredentials(email: string, password: string): this {
this.user.email = email;
this.user.password = password;
return this;
}
withProfile(firstName: string, lastName: string, age: number): this {
this.user.firstName = firstName;
this.user.lastName = lastName;
this.user.age = age;
return this;
}
withAddress(country: string, city: string, street: string, zipCode: string): this {
this.user.address = { country, city, street, zipCode };
return this;
}
withContact(phoneNumber: string): this {
this.user.phoneNumber = phoneNumber;
return this;
}
build(): User {
if (!this.user.email || !this.user.password) {
throw new Error('Email and password required');
}
return this.user as User;
}
}
// Usage
const user = new UserBuilder()
.withCredentials('user@example.com', 'password123')
.withProfile('John', 'Doe', 30)
.withAddress('US', 'New York', '123 Main St', '10001')
.withContact('555-1234')
.build();Data Clumps
Symptoms: Same group of data items appear together in multiple places
Problems:
- Duplication
- Missing abstraction
- Hard to maintain
Detection:
// Red flags:
// - Same parameters appear together
// - Same fields appear in multiple classes
// - Parameters deleted together
class Customer {
name: string;
street: string;
city: string;
state: string;
zipCode: string;
}
class Order {
shippingStreet: string;
shippingCity: string;
shippingState: string;
shippingZipCode: string;
billingStreet: string;
billingCity: string;
billingState: string;
billingZipCode: string;
}
function printAddress(
street: string,
city: string,
state: string,
zipCode: string
) {
// Address parameters always together
}Solutions:
1. Extract Class 2. Introduce Parameter Object 3. Preserve Whole Object
// Fixed: Extract Address class
class Address {
constructor(
public street: string,
public city: string,
public state: string,
public zipCode: string
) {}
format(): string {
return `${this.street}, ${this.city}, ${this.state} ${this.zipCode}`;
}
validate(): boolean {
return Boolean(
this.street &&
this.city &&
this.state &&
/^\d{5}$/.test(this.zipCode)
);
}
}
class Customer {
constructor(
public name: string,
public address: Address
) {}
}
class Order {
constructor(
public shippingAddress: Address,
public billingAddress: Address
) {}
}
function printAddress(address: Address) {
console.log(address.format());
}Object-Orientation Abusers
Incomplete or incorrect application of object-oriented principles.
Switch Statements
Symptoms: Complex switch or if-else chains based on type codes
Problems:
- Violates Open/Closed Principle
- Duplicated switch logic
- Hard to extend
Detection:
// Red flags:
// - Switch on type code
// - Same switch appears in multiple places
// - Adding new type requires changes everywhere
class Employee {
type: 'engineer' | 'manager' | 'salesman';
calculatePay(): number {
switch (this.type) {
case 'engineer':
return this.salary;
case 'manager':
return this.salary + this.bonus;
case 'salesman':
return this.salary + this.commission;
}
}
calculateVacationDays(): number {
switch (this.type) {
case 'engineer':
return 20;
case 'manager':
return 25;
case 'salesman':
return 15;
}
}
getTitle(): string {
switch (this.type) {
case 'engineer':
return 'Software Engineer';
case 'manager':
return 'Engineering Manager';
case 'salesman':
return 'Sales Representative';
}
}
}Solutions:
1. Replace Type Code with Polymorphism 2. Replace Type Code with State/Strategy 3. Replace Conditional with Polymorphism
// Fixed: Polymorphism
abstract class Employee {
constructor(protected salary: number) {}
abstract calculatePay(): number;
abstract calculateVacationDays(): number;
abstract getTitle(): string;
}
class Engineer extends Employee {
calculatePay(): number {
return this.salary;
}
calculateVacationDays(): number {
return 20;
}
getTitle(): string {
return 'Software Engineer';
}
}
class Manager extends Employee {
constructor(salary: number, private bonus: number) {
super(salary);
}
calculatePay(): number {
return this.salary + this.bonus;
}
calculateVacationDays(): number {
return 25;
}
getTitle(): string {
return 'Engineering Manager';
}
}
class Salesman extends Employee {
constructor(salary: number, private commission: number) {
super(salary);
}
calculatePay(): number {
return this.salary + this.commission;
}
calculateVacationDays(): number {
return 15;
}
getTitle(): string {
return 'Sales Representative';
}
}Temporary Field
Symptoms: Field used only in certain circumstances
Problems:
- Confusing - why is field sometimes empty?
- Hard to understand when field is valid
- Often indicates missing abstraction
Detection:
// Red flags:
// - Fields that are null/undefined most of the time
// - Fields used only in specific methods
// - Complex null checking
class Order {
items: OrderItem[];
customer: Customer;
// These are only used during price calculation
basePrice?: number;
discounts?: number;
taxes?: number;
calculateTotal(): number {
this.basePrice = this.calculateBasePrice();
this.discounts = this.calculateDiscounts();
this.taxes = this.calculateTaxes();
return this.basePrice - this.discounts + this.taxes;
}
private calculateBasePrice(): number {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
private calculateDiscounts(): number {
// Complex discount logic
return 0;
}
private calculateTaxes(): number {
// Complex tax logic
return 0;
}
}Solutions:
1. Extract Class 2. Replace Method with Method Object
// Fixed: Extract calculation class
class PriceCalculation {
private basePrice: number;
private discounts: number;
private taxes: number;
constructor(private order: Order) {
this.basePrice = this.calculateBasePrice();
this.discounts = this.calculateDiscounts();
this.taxes = this.calculateTaxes();
}
getTotal(): number {
return this.basePrice - this.discounts + this.taxes;
}
getBasePrice(): number {
return this.basePrice;
}
getDiscounts(): number {
return this.discounts;
}
getTaxes(): number {
return this.taxes;
}
private calculateBasePrice(): number {
return this.order.items.reduce((sum, item) => sum + item.price, 0);
}
private calculateDiscounts(): number {
// Complex discount logic
return 0;
}
private calculateTaxes(): number {
// Complex tax logic
return 0;
}
}
class Order {
items: OrderItem[];
customer: Customer;
calculateTotal(): number {
const calculation = new PriceCalculation(this);
return calculation.getTotal();
}
getPriceBreakdown(): PriceBreakdown {
const calculation = new PriceCalculation(this);
return {
basePrice: calculation.getBasePrice(),
discounts: calculation.getDiscounts(),
taxes: calculation.getTaxes(),
total: calculation.getTotal()
};
}
}Refused Bequest
Symptoms: Subclass uses only some of inherited methods/properties
Problems:
- Wrong hierarchy
- Violates Liskov Substitution Principle
- Confusing interface
Detection:
// Red flags:
// - Subclass throws errors for parent methods
// - Subclass leaves parent methods empty
// - Subclass doesn't use parent fields
class Bird {
fly() {
console.log('Flying');
}
eat() {
console.log('Eating');
}
}
class Penguin extends Bird {
fly() {
throw new Error('Penguins cannot fly!');
}
swim() {
console.log('Swimming');
}
}Solutions:
1. Replace Inheritance with Delegation 2. Extract Superclass 3. Push Down Method/Field
// Fixed: Better hierarchy
interface Bird {
eat(): void;
}
interface FlyingBird extends Bird {
fly(): void;
}
interface SwimmingBird extends Bird {
swim(): void;
}
class Sparrow implements FlyingBird {
fly() {
console.log('Flying');
}
eat() {
console.log('Eating');
}
}
class Penguin implements SwimmingBird {
swim() {
console.log('Swimming');
}
eat() {
console.log('Eating');
}
}Change Preventers
These smells make changes difficult - modifying one thing requires changes in many places.
Divergent Change
Symptoms: One class commonly changed in different ways for different reasons
Problems:
- Violates Single Responsibility Principle
- Hard to maintain
- Changes affect multiple concerns
Detection:
// Red flags:
// - "When we add a new database, we change methods X, Y, Z"
// - "When we add a new payment type, we change methods A, B, C"
// - One class changes for multiple reasons
class Product {
// Database operations
async saveToDatabase() { }
async loadFromDatabase() { }
async deleteFromDatabase() { }
// Price calculations
calculatePrice() { }
applyDiscount() { }
calculateTax() { }
// Display formatting
formatForDisplay() { }
generateHTML() { }
exportToJSON() { }
// Validation
validate() { }
sanitizeInput() { }
}Solutions:
1. Extract Class 2. Split up the behavior
// Fixed: Separate concerns
class Product {
constructor(
public id: string,
public name: string,
public basePrice: number
) {}
}
class ProductRepository {
async save(product: Product): Promise<void> { }
async load(id: string): Promise<Product> { }
async delete(id: string): Promise<void> { }
}
class ProductPricing {
calculatePrice(product: Product): number { }
applyDiscount(price: number, discount: Discount): number { }
calculateTax(price: number): number { }
}
class ProductFormatter {
formatForDisplay(product: Product): string { }
generateHTML(product: Product): string { }
exportToJSON(product: Product): string { }
}
class ProductValidator {
validate(product: Product): ValidationResult { }
sanitize(input: any): Product { }
}This guide helps identify common code smells. For refactoring techniques to fix them, see REFACTORING-PATTERNS.md.
Common Refactoring Scenarios
Legacy Code Refactoring
When working with legacy code without tests:
1. Add Characterization Tests
// Document current behavior before changing
describe('Legacy calculatePrice', () => {
it('returns correct price for scenario A', () => {
expect(calculatePrice(input1)).toBe(expectedOutput1);
});
it('returns correct price for scenario B', () => {
expect(calculatePrice(input2)).toBe(expectedOutput2);
});
});2. Identify Seams
- Find places where you can inject dependencies
- Extract pure functions that can be tested in isolation
3. Refactor Incrementally
- Make one small change
- Run characterization tests
- Commit if tests pass
Performance Refactoring
// Before: N+1 query problem
async function getPostsWithAuthors() {
const posts = await Post.findAll();
for (const post of posts) {
post.author = await User.findById(post.authorId);
}
return posts;
}
// After: Optimized with eager loading
async function getPostsWithAuthors() {
return await Post.findAll({
include: [{ model: User, as: 'author' }]
});
}
// Or with manual optimization
async function getPostsWithAuthors() {
const posts = await Post.findAll();
const authorIds = [...new Set(posts.map(p => p.authorId))];
const authors = await User.findAll({
where: { id: { [Op.in]: authorIds } }
});
const authorMap = new Map(authors.map(a => [a.id, a]));
return posts.map(post => ({
...post,
author: authorMap.get(post.authorId)
}));
}Output Format
When performing refactoring, structure your response as:
1. Analysis
- Identified code smells
- Complexity metrics
- Refactoring opportunities
2. Refactoring Plan
- Prioritized list of refactorings
- Estimated effort and impact
- Dependencies between refactorings
3. Implementation
- Step-by-step refactoring process
- Before and after code examples
- Test coverage verification
4. Validation
- Test results
- Performance impact
- Code quality metrics
Refactoring Best Practices
1. Test-Driven Refactoring
Always refactor with a safety net of tests:
// Step 1: Write tests first (if they don't exist)
describe('UserService.createUser', () => {
it('should create user with valid data', async () => {
const result = await userService.createUser({
email: 'test@example.com',
password: 'password123',
name: 'Test User'
});
expect(result).toMatchObject({
email: 'test@example.com',
name: 'Test User'
});
expect(result.id).toBeDefined();
expect(result.password).not.toBe('password123');
});
it('should throw error for duplicate email', async () => {
await userService.createUser({
email: 'existing@example.com',
password: 'password123',
name: 'First User'
});
await expect(
userService.createUser({
email: 'existing@example.com',
password: 'password456',
name: 'Second User'
})
).rejects.toThrow('User already exists');
});
});
// Step 2: Refactor with confidence
// Step 3: Run tests after each small change
// Step 4: Commit frequently2. Small, Incremental Changes
Break refactoring into small steps:
# Bad: Big bang refactoring
git commit -m "Refactored entire codebase"
# Good: Small, focused commits
git commit -m "Extract validateEmail method from createUser"
git commit -m "Extract hashPassword method from createUser"
git commit -m "Introduce UserRepository interface"
git commit -m "Move database logic to PostgresUserRepository"
git commit -m "Extract CreateUserUseCase from controller"3. Apply SOLID Principles
Single Responsibility Principle
// Before: Multiple responsibilities
class UserService {
createUser(data: any) { }
sendEmail(email: string) { }
validateUser(user: any) { }
generateReport(userId: string) { }
}
// After: Single responsibility
class UserService {
createUser(data: CreateUserDto): Promise<User> { }
updateUser(id: string, data: UpdateUserDto): Promise<User> { }
deleteUser(id: string): Promise<void> { }
}
class EmailService {
send(email: Email): Promise<void> { }
}
class UserValidator {
validate(user: User): ValidationResult { }
}
class UserReportGenerator {
generate(userId: string): Promise<Report> { }
}Open/Closed Principle
// Before: Modification required for new types
class DiscountCalculator {
calculate(order: Order): number {
if (order.customerType === 'regular') {
return order.total * 0.05;
} else if (order.customerType === 'premium') {
return order.total * 0.10;
} else if (order.customerType === 'vip') {
return order.total * 0.15;
}
return 0;
}
}
// After: Extension without modification
interface DiscountStrategy {
calculate(order: Order): number;
}
class RegularDiscount implements DiscountStrategy {
calculate(order: Order): number {
return order.total * 0.05;
}
}
class PremiumDiscount implements DiscountStrategy {
calculate(order: Order): number {
return order.total * 0.10;
}
}
class VIPDiscount implements DiscountStrategy {
calculate(order: Order): number {
return order.total * 0.15;
}
}
class DiscountCalculator {
constructor(private strategy: DiscountStrategy) {}
calculate(order: Order): number {
return this.strategy.calculate(order);
}
}4. Eliminate Duplication (DRY)
// Before: Duplicated validation logic
class UserController {
async createUser(req: Request, res: Response) {
const { email, password } = req.body;
if (!email || !email.includes('@')) {
return res.status(400).json({ error: 'Invalid email' });
}
if (!password || password.length < 8) {
return res.status(400).json({ error: 'Password too short' });
}
// Create user...
}
async updateUser(req: Request, res: Response) {
const { email } = req.body;
if (email && !email.includes('@')) {
return res.status(400).json({ error: 'Invalid email' });
}
// Update user...
}
}
// After: Shared validation logic
class EmailValidator {
static validate(email: string): void {
if (!email || !email.includes('@')) {
throw new ValidationError('Invalid email format');
}
}
}
class PasswordValidator {
static validate(password: string): void {
if (!password || password.length < 8) {
throw new ValidationError('Password must be at least 8 characters');
}
}
}
class UserController {
async createUser(req: Request, res: Response) {
try {
EmailValidator.validate(req.body.email);
PasswordValidator.validate(req.body.password);
// Create user...
} catch (error) {
if (error instanceof ValidationError) {
return res.status(400).json({ error: error.message });
}
throw error;
}
}
async updateUser(req: Request, res: Response) {
try {
if (req.body.email) {
EmailValidator.validate(req.body.email);
}
// Update user...
} catch (error) {
if (error instanceof ValidationError) {
return res.status(400).json({ error: error.message });
}
throw error;
}
}
}Refactoring Patterns Catalog
This reference provides a comprehensive catalog of refactoring patterns with detailed examples and use cases.
Composing Methods
Extract Method
Problem: Code fragment that can be grouped together Solution: Turn the fragment into a method with a descriptive name
// Before
function printOwing() {
printBanner();
// Print details
console.log('name: ' + name);
console.log('amount: ' + getOutstanding());
}
// After
function printOwing() {
printBanner();
printDetails(getOutstanding());
}
function printDetails(outstanding: number) {
console.log('name: ' + name);
console.log('amount: ' + outstanding);
}
// Advanced example: Extract method with local variables
// Before
function calculateTotal(order: Order) {
let basePrice = order.quantity * order.itemPrice;
let discount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
let shipping = Math.min(basePrice * 0.1, 100);
return basePrice - discount + shipping;
}
// After
function calculateTotal(order: Order) {
const basePrice = calculateBasePrice(order);
const discount = calculateDiscount(order);
const shipping = calculateShipping(basePrice);
return basePrice - discount + shipping;
}
function calculateBasePrice(order: Order): number {
return order.quantity * order.itemPrice;
}
function calculateDiscount(order: Order): number {
const discountableQuantity = Math.max(0, order.quantity - 500);
return discountableQuantity * order.itemPrice * 0.05;
}
function calculateShipping(basePrice: number): number {
return Math.min(basePrice * 0.1, 100);
}Inline Method
Problem: Method body is as clear as its name Solution: Replace calls with method body
// Before
function getRating() {
return moreThanFiveLateDeliveries() ? 2 : 1;
}
function moreThanFiveLateDeliveries() {
return numberOfLateDeliveries > 5;
}
// After
function getRating() {
return numberOfLateDeliveries > 5 ? 2 : 1;
}Extract Variable
Problem: Complex expression is hard to understand Solution: Break expression into intermediate variables
// Before
if (
platform.toUpperCase().indexOf('MAC') > -1 &&
browser.toUpperCase().indexOf('IE') > -1 &&
wasInitialized() &&
resize > 0
) {
// Do something
}
// After
const isMacOS = platform.toUpperCase().indexOf('MAC') > -1;
const isIE = browser.toUpperCase().indexOf('IE') > -1;
const wasResized = resize > 0;
if (isMacOS && isIE && wasInitialized() && wasResized) {
// Do something
}Inline Variable
Problem: Variable doesn't provide more clarity than expression Solution: Replace variable references with expression
// Before
const basePrice = order.basePrice;
return basePrice > 1000;
// After
return order.basePrice > 1000;Replace Temp with Query
Problem: Temporary variable holds result of expression Solution: Extract expression into method, replace all references
// Before
function calculateTotal(order: Order) {
const basePrice = order.quantity * order.itemPrice;
if (basePrice > 1000) {
return basePrice * 0.95;
}
return basePrice * 0.98;
}
// After
function calculateTotal(order: Order) {
if (basePrice(order) > 1000) {
return basePrice(order) * 0.95;
}
return basePrice(order) * 0.98;
}
function basePrice(order: Order): number {
return order.quantity * order.itemPrice;
}Split Temporary Variable
Problem: Temporary variable assigned more than once (excluding loops) Solution: Use separate variables for each assignment
// Before
let temp = 2 * (height + width);
console.log(temp);
temp = height * width;
console.log(temp);
// After
const perimeter = 2 * (height + width);
console.log(perimeter);
const area = height * width;
console.log(area);Remove Assignments to Parameters
Problem: Code assigns values to parameters Solution: Use local variable instead
// Before
function discount(inputVal: number, quantity: number): number {
if (inputVal > 50) inputVal -= 2;
if (quantity > 100) inputVal -= 1;
return inputVal;
}
// After
function discount(inputVal: number, quantity: number): number {
let result = inputVal;
if (inputVal > 50) result -= 2;
if (quantity > 100) result -= 1;
return result;
}Replace Method with Method Object
Problem: Long method with many local variables that can't be extracted Solution: Create class with local variables as fields
// Before
function price(order: Order) {
let primaryBasePrice;
let secondaryBasePrice;
let tertiaryBasePrice;
// Long calculation with these variables
}
// After
class PriceCalculator {
private primaryBasePrice: number;
private secondaryBasePrice: number;
private tertiaryBasePrice: number;
constructor(private order: Order) {}
compute(): number {
this.primaryBasePrice = this.calculatePrimaryBasePrice();
this.secondaryBasePrice = this.calculateSecondaryBasePrice();
this.tertiaryBasePrice = this.calculateTertiaryBasePrice();
return this.computeFinalPrice();
}
private calculatePrimaryBasePrice(): number {
// Calculation
}
private calculateSecondaryBasePrice(): number {
// Calculation
}
private calculateTertiaryBasePrice(): number {
// Calculation
}
private computeFinalPrice(): number {
// Final calculation
}
}
function price(order: Order): number {
return new PriceCalculator(order).compute();
}Moving Features Between Objects
Move Method
Problem: Method used more in another class Solution: Create new method in target class, delegate or remove old method
// Before
class Account {
overdraftCharge() {
if (this.type.isPremium()) {
// Premium overdraft calculation
return this.daysOverdrawn * 2.5;
} else {
// Standard overdraft calculation
return this.daysOverdrawn * 1.75;
}
}
}
// After
class AccountType {
overdraftCharge(daysOverdrawn: number): number {
if (this.isPremium()) {
return daysOverdrawn * 2.5;
} else {
return daysOverdrawn * 1.75;
}
}
}
class Account {
overdraftCharge() {
return this.type.overdraftCharge(this.daysOverdrawn);
}
}Move Field
Problem: Field used more in another class Solution: Create field in target class, redirect all users
// Before
class Customer {
private discountRate: number;
getDiscountRate(): number {
return this.discountRate;
}
}
class Order {
getDiscountedPrice(): number {
return this.basePrice * (1 - this.customer.getDiscountRate());
}
}
// After (if discount is based on customer type)
class CustomerType {
private discountRate: number;
getDiscountRate(): number {
return this.discountRate;
}
}
class Customer {
getDiscountRate(): number {
return this.type.getDiscountRate();
}
}
class Order {
getDiscountedPrice(): number {
return this.basePrice * (1 - this.customer.getDiscountRate());
}
}Extract Class
Problem: Class doing work of two Solution: Create new class, move relevant fields and methods
// Before
class Person {
name: string;
officeAreaCode: string;
officeNumber: string;
getTelephoneNumber(): string {
return `(${this.officeAreaCode}) ${this.officeNumber}`;
}
}
// After
class TelephoneNumber {
constructor(
private areaCode: string,
private number: string
) {}
toString(): string {
return `(${this.areaCode}) ${this.number}`;
}
getAreaCode(): string {
return this.areaCode;
}
getNumber(): string {
return this.number;
}
}
class Person {
name: string;
private officeTelephone: TelephoneNumber;
getTelephoneNumber(): string {
return this.officeTelephone.toString();
}
getOfficeTelephone(): TelephoneNumber {
return this.officeTelephone;
}
}Inline Class
Problem: Class not doing much Solution: Move all features to another class, delete original
// Before
class Address {
constructor(private zipCode: string) {}
getZipCode(): string {
return this.zipCode;
}
}
class Person {
constructor(
private name: string,
private address: Address
) {}
getZipCode(): string {
return this.address.getZipCode();
}
}
// After
class Person {
constructor(
private name: string,
private zipCode: string
) {}
getZipCode(): string {
return this.zipCode;
}
}Hide Delegate
Problem: Client gets object from field of server object, then calls method Solution: Create method on server that hides the delegate
// Before
class Person {
department: Department;
}
class Department {
manager: Person;
getManager(): Person {
return this.manager;
}
}
// Client code
const manager = john.department.getManager();
// After
class Person {
private department: Department;
getManager(): Person {
return this.department.getManager();
}
}
// Client code
const manager = john.getManager();Remove Middle Man
Problem: Class doing too much delegating Solution: Get client to call delegate directly
// Before (too much delegation)
class Person {
private department: Department;
getManager(): Person {
return this.department.getManager();
}
getBudget(): number {
return this.department.getBudget();
}
getLocation(): string {
return this.department.getLocation();
}
// Many more delegating methods...
}
// After
class Person {
getDepartment(): Department {
return this.department;
}
}
// Client code
const manager = john.getDepartment().getManager();Organizing Data
Self Encapsulate Field
Problem: Direct access to private field Solution: Use getters and setters
// Before
class Range {
private low: number;
private high: number;
includes(arg: number): boolean {
return arg >= this.low && arg <= this.high;
}
}
// After
class Range {
private low: number;
private high: number;
includes(arg: number): boolean {
return arg >= this.getLow() && arg <= this.getHigh();
}
getLow(): number {
return this.low;
}
getHigh(): number {
return this.high;
}
}Replace Data Value with Object
Problem: Data item needs additional data or behavior Solution: Turn data item into object
// Before
class Order {
customer: string;
constructor(customerName: string) {
this.customer = customerName;
}
}
// After
class Customer {
constructor(private name: string) {}
getName(): string {
return this.name;
}
}
class Order {
customer: Customer;
constructor(customerName: string) {
this.customer = new Customer(customerName);
}
getCustomerName(): string {
return this.customer.getName();
}
}Change Value to Reference
Problem: Many identical instances of a class need to be single object Solution: Turn object into reference object
// Before
class Customer {
constructor(private name: string) {}
}
class Order {
private customer: Customer;
constructor(customerName: string) {
this.customer = new Customer(customerName);
}
}
// After
class Customer {
private static instances = new Map<string, Customer>();
private constructor(private name: string) {}
static get(name: string): Customer {
if (!Customer.instances.has(name)) {
Customer.instances.set(name, new Customer(name));
}
return Customer.instances.get(name)!;
}
}
class Order {
private customer: Customer;
constructor(customerName: string) {
this.customer = Customer.get(customerName);
}
}Replace Array with Object
Problem: Array elements mean different things Solution: Replace array with object with fields for each element
// Before
const row = ['Liverpool', '15'];
const name = row[0];
const wins = parseInt(row[1]);
// After
interface Performance {
name: string;
wins: number;
}
const row: Performance = { name: 'Liverpool', wins: 15 };
const name = row.name;
const wins = row.wins;Duplicate Observed Data
Problem: Domain data stored in GUI components Solution: Copy data to domain object, observe changes
// Before
class TextField {
private text: string;
getText(): string {
return this.text;
}
setText(value: string) {
this.text = value;
// Update view
}
}
// After
interface Observer {
update(value: string): void;
}
class DomainData {
private observers: Observer[] = [];
private value: string;
getValue(): string {
return this.value;
}
setValue(value: string) {
this.value = value;
this.notifyObservers();
}
addObserver(observer: Observer) {
this.observers.push(observer);
}
private notifyObservers() {
for (const observer of this.observers) {
observer.update(this.value);
}
}
}
class TextField implements Observer {
private text: string;
constructor(private data: DomainData) {
data.addObserver(this);
}
getText(): string {
return this.text;
}
setText(value: string) {
this.text = value;
this.data.setValue(value);
}
update(value: string) {
this.text = value;
// Update view
}
}Change Unidirectional Association to Bidirectional
Problem: Two classes need to use each other's features Solution: Add back-pointers
// Before
class Order {
customer: Customer;
getCustomer(): Customer {
return this.customer;
}
}
class Customer {
// No reference to orders
}
// After
class Order {
private customer: Customer;
constructor(customer: Customer) {
this.customer = customer;
customer.addOrder(this);
}
getCustomer(): Customer {
return this.customer;
}
setCustomer(customer: Customer) {
if (this.customer) {
this.customer.removeOrder(this);
}
this.customer = customer;
customer.addOrder(this);
}
}
class Customer {
private orders: Set<Order> = new Set();
addOrder(order: Order) {
this.orders.add(order);
}
removeOrder(order: Order) {
this.orders.delete(order);
}
getOrders(): Order[] {
return Array.from(this.orders);
}
}Replace Magic Number with Symbolic Constant
Problem: Numeric literal with special meaning Solution: Create constant with human-readable name
// Before
function potentialEnergy(mass: number, height: number): number {
return mass * 9.81 * height;
}
// After
const GRAVITATIONAL_CONSTANT = 9.81;
function potentialEnergy(mass: number, height: number): number {
return mass * GRAVITATIONAL_CONSTANT * height;
}Encapsulate Field
Problem: Public field Solution: Make private and provide accessors
// Before
class Person {
name: string;
}
// After
class Person {
private name: string;
getName(): string {
return this.name;
}
setName(name: string) {
this.name = name;
}
}Encapsulate Collection
Problem: Method returns collection Solution: Return read-only view, provide add/remove methods
// Before
class Course {
private students: Student[] = [];
getStudents(): Student[] {
return this.students;
}
setStudents(students: Student[]) {
this.students = students;
}
}
// Usage
const course = new Course();
course.getStudents().push(new Student('John')); // Direct manipulation!
// After
class Course {
private students: Student[] = [];
getStudents(): ReadonlyArray<Student> {
return Object.freeze([...this.students]);
}
addStudent(student: Student) {
this.students.push(student);
}
removeStudent(student: Student) {
const index = this.students.indexOf(student);
if (index !== -1) {
this.students.splice(index, 1);
}
}
getNumberOfStudents(): number {
return this.students.length;
}
}
// Usage
const course = new Course();
course.addStudent(new Student('John'));Replace Type Code with Class
Problem: Class has type code that affects behavior Solution: Replace with class or enum
// Before
class Person {
static readonly O = 0;
static readonly A = 1;
static readonly B = 2;
static readonly AB = 3;
private bloodGroup: number;
constructor(bloodGroup: number) {
this.bloodGroup = bloodGroup;
}
}
// After
class BloodGroup {
static readonly O = new BloodGroup('O');
static readonly A = new BloodGroup('A');
static readonly B = new BloodGroup('B');
static readonly AB = new BloodGroup('AB');
private constructor(private code: string) {}
getCode(): string {
return this.code;
}
}
class Person {
private bloodGroup: BloodGroup;
constructor(bloodGroup: BloodGroup) {
this.bloodGroup = bloodGroup;
}
getBloodGroup(): BloodGroup {
return this.bloodGroup;
}
}
// Or with TypeScript enum
enum BloodGroup {
O = 'O',
A = 'A',
B = 'B',
AB = 'AB'
}
class Person {
constructor(private bloodGroup: BloodGroup) {}
getBloodGroup(): BloodGroup {
return this.bloodGroup;
}
}Replace Type Code with Subclasses
Problem: Type code affects class behavior Solution: Create subclass for each type code value
// Before
class Employee {
private type: string;
constructor(type: string) {
this.type = type;
}
payAmount(): number {
switch (this.type) {
case 'engineer':
return this.monthlySalary;
case 'salesman':
return this.monthlySalary + this.commission;
case 'manager':
return this.monthlySalary + this.bonus;
default:
throw new Error('Invalid employee type');
}
}
}
// After
abstract class Employee {
constructor(protected monthlySalary: number) {}
abstract payAmount(): number;
}
class Engineer extends Employee {
payAmount(): number {
return this.monthlySalary;
}
}
class Salesman extends Employee {
constructor(
monthlySalary: number,
private commission: number
) {
super(monthlySalary);
}
payAmount(): number {
return this.monthlySalary + this.commission;
}
}
class Manager extends Employee {
constructor(
monthlySalary: number,
private bonus: number
) {
super(monthlySalary);
}
payAmount(): number {
return this.monthlySalary + this.bonus;
}
}This catalog provides foundational refactoring patterns. For code smell identification and testing strategies, see the companion reference files.
Refactoring Process
Follow this systematic approach when refactoring code:
Phase 1: Assessment & Planning
1. Identify Refactoring Needs
- Code review findings
- Static analysis tool reports (SonarQube, ESLint, etc.)
- Performance profiling results
- Developer pain points
- Frequent bug locations
- Difficulty adding new features
2. Analyze Code Smells
- Bloaters: Long methods (>20 lines), large classes (>300 lines), long parameter lists (>3 params)
- Object-Orientation Abusers: Switch statements, refused bequest, temporary fields
- Change Preventers: Divergent change, shotgun surgery, parallel inheritance
- Dispensables: Comments (excessive), duplicate code, dead code, speculative generality
- Couplers: Feature envy, inappropriate intimacy, message chains, middle man
3. Establish Safety Net
// Before refactoring, ensure comprehensive tests exist
describe('UserService', () => {
it('should create user with valid data', async () => {
const userData = { email: 'test@example.com', name: 'Test User' };
const user = await userService.createUser(userData);
expect(user).toBeDefined();
expect(user.email).toBe(userData.email);
});
it('should throw error for invalid email', async () => {
const userData = { email: 'invalid', name: 'Test' };
await expect(userService.createUser(userData)).rejects.toThrow();
});
it('should hash password before saving', async () => {
const userData = { email: 'test@example.com', password: 'plain' };
const user = await userService.createUser(userData);
expect(user.password).not.toBe('plain');
expect(user.password.length).toBeGreaterThan(20);
});
});4. Prioritize Refactoring Tasks
- High impact, low effort (quick wins)
- Critical bugs or security issues
- Code touched frequently
- Code blocking new features
- Code with high cyclomatic complexity
Phase 2: Method-Level Refactoring
Extract Method: Break down long methods
// Before: Long method doing too much
class OrderProcessor {
async processOrder(orderId: string) {
const order = await this.orderRepository.findById(orderId);
if (!order) throw new Error('Order not found');
// Validate inventory
for (const item of order.items) {
const product = await this.productRepository.findById(item.productId);
if (!product) throw new Error('Product not found');
if (product.stock < item.quantity) {
throw new Error('Insufficient stock');
}
}
// Calculate total
let total = 0;
for (const item of order.items) {
const product = await this.productRepository.findById(item.productId);
total += product.price * item.quantity;
}
if (order.couponCode) {
const coupon = await this.couponRepository.findByCode(order.couponCode);
if (coupon && coupon.isValid()) {
total = total * (1 - coupon.discount);
}
}
// Process payment
const payment = await this.paymentService.charge({
amount: total,
customerId: order.customerId
});
// Update inventory
for (const item of order.items) {
await this.productRepository.decrementStock(item.productId, item.quantity);
}
order.status = 'completed';
order.total = total;
order.paymentId = payment.id;
await this.orderRepository.save(order);
return order;
}
}
// After: Extracted into focused methods
class OrderProcessor {
async processOrder(orderId: string): Promise<Order> {
const order = await this.getOrder(orderId);
await this.validateInventory(order);
const total = await this.calculateTotal(order);
const payment = await this.processPayment(order.customerId, total);
await this.updateInventory(order);
return await this.completeOrder(order, total, payment.id);
}
private async getOrder(orderId: string): Promise<Order> {
const order = await this.orderRepository.findById(orderId);
if (!order) {
throw new NotFoundError('Order not found');
}
return order;
}
private async validateInventory(order: Order): Promise<void> {
for (const item of order.items) {
const product = await this.productRepository.findById(item.productId);
if (!product) {
throw new NotFoundError(`Product ${item.productId} not found`);
}
if (product.stock < item.quantity) {
throw new InsufficientStockError(product.id, item.quantity, product.stock);
}
}
}
private async calculateTotal(order: Order): Promise<number> {
const subtotal = await this.calculateSubtotal(order.items);
const discount = await this.calculateDiscount(order.couponCode, subtotal);
return subtotal - discount;
}
private async calculateSubtotal(items: OrderItem[]): Promise<number> {
let total = 0;
for (const item of items) {
const product = await this.productRepository.findById(item.productId);
total += product.price * item.quantity;
}
return total;
}
private async calculateDiscount(couponCode: string | null, subtotal: number): Promise<number> {
if (!couponCode) return 0;
const coupon = await this.couponRepository.findByCode(couponCode);
if (!coupon || !coupon.isValid()) return 0;
return subtotal * coupon.discount;
}
private async processPayment(customerId: string, amount: number): Promise<Payment> {
return await this.paymentService.charge({ amount, customerId });
}
private async updateInventory(order: Order): Promise<void> {
for (const item of order.items) {
await this.productRepository.decrementStock(item.productId, item.quantity);
}
}
private async completeOrder(order: Order, total: number, paymentId: string): Promise<Order> {
order.status = 'completed';
order.total = total;
order.paymentId = paymentId;
return await this.orderRepository.save(order);
}
}Replace Conditional with Polymorphism
// Before: Complex conditionals
class PaymentProcessor {
processPayment(payment: Payment) {
if (payment.type === 'credit_card') {
this.validateCreditCard(payment);
return this.chargeCreditCard(payment);
} else if (payment.type === 'paypal') {
this.validatePayPal(payment);
return this.chargePayPal(payment);
} else if (payment.type === 'bank_transfer') {
this.validateBankTransfer(payment);
return this.chargeBankTransfer(payment);
} else {
throw new Error('Unknown payment type');
}
}
}
// After: Polymorphic payment methods
interface PaymentMethod {
validate(): void;
charge(amount: number): Promise<PaymentResult>;
}
class CreditCardPayment implements PaymentMethod {
constructor(private cardNumber: string, private cvv: string, private expiry: string) {}
validate(): void {
if (!this.isValidCardNumber(this.cardNumber)) {
throw new ValidationError('Invalid card number');
}
if (!this.isValidCVV(this.cvv)) {
throw new ValidationError('Invalid CVV');
}
if (this.isExpired(this.expiry)) {
throw new ValidationError('Card expired');
}
}
async charge(amount: number): Promise<PaymentResult> {
this.validate();
return await this.creditCardGateway.charge({
cardNumber: this.cardNumber,
cvv: this.cvv,
expiry: this.expiry,
amount
});
}
private isValidCardNumber(cardNumber: string): boolean {
// Luhn algorithm
return true;
}
private isValidCVV(cvv: string): boolean {
return /^\d{3,4}$/.test(cvv);
}
private isExpired(expiry: string): boolean {
const [month, year] = expiry.split('/');
const expiryDate = new Date(parseInt('20' + year), parseInt(month) - 1);
return expiryDate < new Date();
}
}
class PayPalPayment implements PaymentMethod {
constructor(private email: string, private token: string) {}
validate(): void {
if (!this.isValidEmail(this.email)) {
throw new ValidationError('Invalid email');
}
if (!this.token) {
throw new ValidationError('Token required');
}
}
async charge(amount: number): Promise<PaymentResult> {
this.validate();
return await this.paypalGateway.charge({
email: this.email,
token: this.token,
amount
});
}
private isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
}
class BankTransferPayment implements PaymentMethod {
constructor(private accountNumber: string, private routingNumber: string) {}
validate(): void {
if (!this.isValidAccountNumber(this.accountNumber)) {
throw new ValidationError('Invalid account number');
}
if (!this.isValidRoutingNumber(this.routingNumber)) {
throw new ValidationError('Invalid routing number');
}
}
async charge(amount: number): Promise<PaymentResult> {
this.validate();
return await this.bankTransferGateway.initiate({
accountNumber: this.accountNumber,
routingNumber: this.routingNumber,
amount
});
}
private isValidAccountNumber(accountNumber: string): boolean {
return /^\d{8,17}$/.test(accountNumber);
}
private isValidRoutingNumber(routingNumber: string): boolean {
return /^\d{9}$/.test(routingNumber);
}
}
// Refactored processor
class PaymentProcessor {
async processPayment(paymentMethod: PaymentMethod, amount: number): Promise<PaymentResult> {
return await paymentMethod.charge(amount);
}
}Phase 3: Class-Level Refactoring
Extract Class: Separate responsibilities
// Before: God class doing too much
class User {
id: string;
email: string;
password: string;
name: string;
avatar: string;
bio: string;
// Authentication logic
async login(password: string): Promise<string> {
const isValid = await bcrypt.compare(password, this.password);
if (!isValid) throw new Error('Invalid credentials');
return jwt.sign({ userId: this.id }, process.env.JWT_SECRET);
}
// Profile management
updateProfile(data: Partial<User>) {
this.name = data.name || this.name;
this.avatar = data.avatar || this.avatar;
this.bio = data.bio || this.bio;
}
// Email sending
async sendWelcomeEmail() {
const transporter = nodemailer.createTransport({...});
await transporter.sendMail({
to: this.email,
subject: 'Welcome!',
html: '<h1>Welcome to our platform</h1>'
});
}
// Notification preferences
emailNotifications: boolean;
pushNotifications: boolean;
updateNotificationPreferences(email: boolean, push: boolean) {
this.emailNotifications = email;
this.pushNotifications = push;
}
}
// After: Separated into focused classes
class User {
id: string;
email: string;
password: string;
name: string;
constructor(data: UserData) {
this.id = data.id;
this.email = data.email;
this.password = data.password;
this.name = data.name;
}
}
class UserProfile {
userId: string;
avatar: string;
bio: string;
update(data: Partial<UserProfile>): void {
if (data.avatar) this.avatar = data.avatar;
if (data.bio) this.bio = data.bio;
}
}
class UserAuthentication {
constructor(
private user: User,
private jwtService: JwtService,
private hashingService: HashingService
) {}
async login(password: string): Promise<string> {
const isValid = await this.hashingService.compare(password, this.user.password);
if (!isValid) {
throw new InvalidCredentialsError();
}
return this.jwtService.sign({ userId: this.user.id });
}
async changePassword(oldPassword: string, newPassword: string): Promise<void> {
const isValid = await this.hashingService.compare(oldPassword, this.user.password);
if (!isValid) {
throw new InvalidCredentialsError();
}
this.user.password = await this.hashingService.hash(newPassword);
}
}
class NotificationPreferences {
userId: string;
emailEnabled: boolean;
pushEnabled: boolean;
smsEnabled: boolean;
update(preferences: Partial<NotificationPreferences>): void {
if (preferences.emailEnabled !== undefined) {
this.emailEnabled = preferences.emailEnabled;
}
if (preferences.pushEnabled !== undefined) {
this.pushEnabled = preferences.pushEnabled;
}
if (preferences.smsEnabled !== undefined) {
this.smsEnabled = preferences.smsEnabled;
}
}
}
class UserNotificationService {
constructor(
private emailService: EmailService,
private preferencesRepository: NotificationPreferencesRepository
) {}
async sendWelcomeEmail(user: User): Promise<void> {
const preferences = await this.preferencesRepository.findByUserId(user.id);
if (!preferences.emailEnabled) return;
await this.emailService.send({
to: user.email,
subject: 'Welcome!',
template: 'welcome',
data: { name: user.name }
});
}
}Introduce Parameter Object
// Before: Long parameter list
function createUser(
email: string,
password: string,
name: string,
age: number,
country: string,
city: string,
zipCode: string,
phoneNumber: string
) {
// Implementation
}
// After: Parameter object
interface CreateUserParams {
email: string;
password: string;
name: string;
age: number;
address: {
country: string;
city: string;
zipCode: string;
};
phoneNumber: string;
}
function createUser(params: CreateUserParams) {
// Implementation with better organization
}
// Even better: Multiple focused parameter objects
interface UserCredentials {
email: string;
password: string;
}
interface UserProfile {
name: string;
age: number;
}
interface Address {
country: string;
city: string;
zipCode: string;
}
interface ContactInfo {
phoneNumber: string;
email: string;
}
function createUser(
credentials: UserCredentials,
profile: UserProfile,
address: Address,
contact: ContactInfo
) {
// Clear, organized parameters
}Phase 4: Architecture-Level Refactoring
Move Toward Clean Architecture
// Before: Mixed concerns, tight coupling
class UserController {
async createUser(req: Request, res: Response) {
const { email, password, name } = req.body;
// Validation mixed with business logic
if (!email || !email.includes('@')) {
return res.status(400).json({ error: 'Invalid email' });
}
// Direct database access from controller
const existingUser = await db.query(
'SELECT * FROM users WHERE email = $1',
[email]
);
if (existingUser.rows.length > 0) {
return res.status(409).json({ error: 'User exists' });
}
// Business logic in controller
const hashedPassword = await bcrypt.hash(password, 10);
const result = await db.query(
'INSERT INTO users (email, password, name) VALUES ($1, $2, $3) RETURNING *',
[email, hashedPassword, name]
);
// Email sending in controller
await sendEmail(email, 'Welcome!', 'Welcome to our platform');
res.status(201).json(result.rows[0]);
}
}
// After: Clean architecture with separated concerns
// Domain layer
class User {
constructor(
public readonly id: string,
public readonly email: Email,
public readonly password: Password,
public readonly name: string
) {}
static create(email: string, password: string, name: string): User {
return new User(
generateId(),
Email.create(email),
Password.create(password),
name
);
}
}
class Email {
private constructor(public readonly value: string) {}
static create(email: string): Email {
if (!email || !email.includes('@')) {
throw new ValidationError('Invalid email format');
}
return new Email(email);
}
}
class Password {
private constructor(public readonly hashedValue: string) {}
static async create(plainPassword: string): Promise<Password> {
if (plainPassword.length < 8) {
throw new ValidationError('Password must be at least 8 characters');
}
const hashed = await bcrypt.hash(plainPassword, 10);
return new Password(hashed);
}
}
// Application layer (use cases)
interface UserRepository {
findByEmail(email: Email): Promise<User | null>;
save(user: User): Promise<void>;
}
interface EventPublisher {
publish(event: DomainEvent): Promise<void>;
}
class CreateUserUseCase {
constructor(
private userRepository: UserRepository,
private eventPublisher: EventPublisher
) {}
async execute(command: CreateUserCommand): Promise<User> {
// Check if user exists
const existingUser = await this.userRepository.findByEmail(
Email.create(command.email)
);
if (existingUser) {
throw new UserAlreadyExistsError(command.email);
}
// Create user
const password = await Password.create(command.password);
const user = new User(
generateId(),
Email.create(command.email),
password,
command.name
);
// Save user
await this.userRepository.save(user);
// Publish event
await this.eventPublisher.publish(
new UserCreatedEvent(user.id, user.email.value)
);
return user;
}
}
// Infrastructure layer
class PostgresUserRepository implements UserRepository {
constructor(private db: Database) {}
async findByEmail(email: Email): Promise<User | null> {
const result = await this.db.query(
'SELECT * FROM users WHERE email = $1',
[email.value]
);
if (result.rows.length === 0) return null;
return this.mapToDomain(result.rows[0]);
}
async save(user: User): Promise<void> {
await this.db.query(
'INSERT INTO users (id, email, password, name) VALUES ($1, $2, $3, $4)',
[user.id, user.email.value, user.password.hashedValue, user.name]
);
}
private mapToDomain(row: any): User {
return new User(
row.id,
Email.create(row.email),
{ hashedValue: row.password } as Password,
row.name
);
}
}
// Presentation layer
class UserController {
constructor(private createUserUseCase: CreateUserUseCase) {}
async createUser(req: Request, res: Response): Promise<void> {
try {
const command = new CreateUserCommand(
req.body.email,
req.body.password,
req.body.name
);
const user = await this.createUserUseCase.execute(command);
res.status(201).json({
id: user.id,
email: user.email.value,
name: user.name
});
} catch (error) {
if (error instanceof ValidationError) {
res.status(400).json({ error: error.message });
} else if (error instanceof UserAlreadyExistsError) {
res.status(409).json({ error: error.message });
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
}
}Testing Strategies for Safe Refactoring
This reference provides comprehensive strategies for safely refactoring code through effective testing practices.
The Testing Safety Net
Before refactoring, establish a comprehensive safety net of tests. The goal is to verify behavior doesn't change during refactoring.
Testing Pyramid for Refactoring
/\
/ \ Unit Tests (70%)
/____\
/ \
/ Integr \ Integration Tests (20%)
/__________\
/ \
/ E2E \ End-to-End Tests (10%)
/________________\Priorities for Refactoring:
1. Before refactoring: Comprehensive tests at appropriate levels 2. During refactoring: Tests remain green 3. After refactoring: Tests still pass, code is cleaner
Test-Driven Refactoring Workflow
Step 1: Add Tests (if missing)
// Legacy code without tests
class OrderProcessor {
processOrder(order: Order): ProcessedOrder {
// Complex logic without tests
const total = order.items.reduce((sum, item) => {
const price = item.price * item.quantity;
const discount = item.discount || 0;
return sum + (price - discount);
}, 0);
const tax = total * 0.08;
const shipping = total > 100 ? 0 : 10;
return {
orderId: order.id,
subtotal: total,
tax,
shipping,
total: total + tax + shipping
};
}
}
// Step 1: Add characterization tests documenting current behavior
describe('OrderProcessor', () => {
let processor: OrderProcessor;
beforeEach(() => {
processor = new OrderProcessor();
});
describe('processOrder', () => {
it('should calculate subtotal correctly', () => {
const order = {
id: '123',
items: [
{ price: 10, quantity: 2, discount: 0 },
{ price: 20, quantity: 1, discount: 0 }
]
};
const result = processor.processOrder(order);
expect(result.subtotal).toBe(40);
});
it('should apply discounts', () => {
const order = {
id: '123',
items: [
{ price: 100, quantity: 1, discount: 10 }
]
};
const result = processor.processOrder(order);
expect(result.subtotal).toBe(90);
});
it('should calculate 8% tax', () => {
const order = {
id: '123',
items: [{ price: 100, quantity: 1, discount: 0 }]
};
const result = processor.processOrder(order);
expect(result.tax).toBe(8);
});
it('should add $10 shipping for orders under $100', () => {
const order = {
id: '123',
items: [{ price: 50, quantity: 1, discount: 0 }]
};
const result = processor.processOrder(order);
expect(result.shipping).toBe(10);
});
it('should have free shipping for orders over $100', () => {
const order = {
id: '123',
items: [{ price: 150, quantity: 1, discount: 0 }]
};
const result = processor.processOrder(order);
expect(result.shipping).toBe(0);
});
it('should calculate correct total', () => {
const order = {
id: '123',
items: [{ price: 100, quantity: 1, discount: 0 }]
};
const result = processor.processOrder(order);
// 100 + 8 (tax) + 0 (shipping)
expect(result.total).toBe(108);
});
});
});Step 2: Refactor with Green Tests
// Refactor: Extract methods
class OrderProcessor {
processOrder(order: Order): ProcessedOrder {
const subtotal = this.calculateSubtotal(order.items);
const tax = this.calculateTax(subtotal);
const shipping = this.calculateShipping(subtotal);
return {
orderId: order.id,
subtotal,
tax,
shipping,
total: subtotal + tax + shipping
};
}
private calculateSubtotal(items: OrderItem[]): number {
return items.reduce((sum, item) => {
const price = item.price * item.quantity;
const discount = item.discount || 0;
return sum + (price - discount);
}, 0);
}
private calculateTax(subtotal: number): number {
return subtotal * 0.08;
}
private calculateShipping(subtotal: number): number {
return subtotal > 100 ? 0 : 10;
}
}
// Run tests - should still be green!
// npm testStep 3: Further Refactoring
// Extract classes for single responsibilities
class SubtotalCalculator {
calculate(items: OrderItem[]): number {
return items.reduce((sum, item) => {
return sum + this.calculateItemTotal(item);
}, 0);
}
private calculateItemTotal(item: OrderItem): number {
const price = item.price * item.quantity;
const discount = item.discount || 0;
return price - discount;
}
}
class TaxCalculator {
private readonly TAX_RATE = 0.08;
calculate(amount: number): number {
return amount * this.TAX_RATE;
}
}
class ShippingCalculator {
private readonly FREE_SHIPPING_THRESHOLD = 100;
private readonly STANDARD_SHIPPING_FEE = 10;
calculate(subtotal: number): number {
return subtotal > this.FREE_SHIPPING_THRESHOLD
? 0
: this.STANDARD_SHIPPING_FEE;
}
}
class OrderProcessor {
constructor(
private subtotalCalculator: SubtotalCalculator,
private taxCalculator: TaxCalculator,
private shippingCalculator: ShippingCalculator
) {}
processOrder(order: Order): ProcessedOrder {
const subtotal = this.subtotalCalculator.calculate(order.items);
const tax = this.taxCalculator.calculate(subtotal);
const shipping = this.shippingCalculator.calculate(subtotal);
return {
orderId: order.id,
subtotal,
tax,
shipping,
total: subtotal + tax + shipping
};
}
}
// Tests still pass! Now we can test components independently
describe('SubtotalCalculator', () => {
it('should calculate item total with discount', () => {
const calculator = new SubtotalCalculator();
const items = [{ price: 100, quantity: 1, discount: 10 }];
expect(calculator.calculate(items)).toBe(90);
});
});
describe('TaxCalculator', () => {
it('should calculate 8% tax', () => {
const calculator = new TaxCalculator();
expect(calculator.calculate(100)).toBe(8);
});
});
describe('ShippingCalculator', () => {
it('should charge shipping for orders under $100', () => {
const calculator = new ShippingCalculator();
expect(calculator.calculate(50)).toBe(10);
});
it('should provide free shipping for orders over $100', () => {
const calculator = new ShippingCalculator();
expect(calculator.calculate(150)).toBe(0);
});
});Testing Strategies by Refactoring Type
Extract Method Refactoring
// Before refactoring: Test the entire method
describe('UserService.createUser', () => {
it('should create user with hashed password', async () => {
const service = new UserService();
const user = await service.createUser({
email: 'test@example.com',
password: 'plain123',
name: 'Test User'
});
expect(user.email).toBe('test@example.com');
expect(user.password).not.toBe('plain123');
expect(user.password.length).toBeGreaterThan(20);
});
});
// After extract method: Test extracted methods independently
describe('PasswordHasher', () => {
it('should hash password with bcrypt', async () => {
const hasher = new PasswordHasher();
const hashed = await hasher.hash('plain123');
expect(hashed).not.toBe('plain123');
expect(hashed.length).toBeGreaterThan(20);
});
it('should verify hashed password', async () => {
const hasher = new PasswordHasher();
const hashed = await hasher.hash('plain123');
expect(await hasher.verify('plain123', hashed)).toBe(true);
expect(await hasher.verify('wrong', hashed)).toBe(false);
});
});
describe('UserService.createUser', () => {
it('should create user with hashed password', async () => {
const mockHasher = {
hash: jest.fn().mockResolvedValue('hashed_password')
};
const service = new UserService(mockHasher);
const user = await service.createUser({
email: 'test@example.com',
password: 'plain123',
name: 'Test User'
});
expect(mockHasher.hash).toHaveBeenCalledWith('plain123');
expect(user.password).toBe('hashed_password');
});
});Extract Class Refactoring
// Before: Single class with multiple responsibilities
class User {
async sendWelcomeEmail() {
const transporter = nodemailer.createTransport({...});
await transporter.sendMail({
to: this.email,
subject: 'Welcome',
html: '<h1>Welcome!</h1>'
});
}
}
// Test before refactoring
describe('User.sendWelcomeEmail', () => {
it('should send welcome email', async () => {
const mockTransport = {
sendMail: jest.fn().mockResolvedValue({ messageId: '123' })
};
jest.spyOn(nodemailer, 'createTransport').mockReturnValue(mockTransport);
const user = new User({ email: 'test@example.com' });
await user.sendWelcomeEmail();
expect(mockTransport.sendMail).toHaveBeenCalledWith(
expect.objectContaining({
to: 'test@example.com',
subject: 'Welcome'
})
);
});
});
// After refactoring: Separate email service
class EmailService {
constructor(private transporter: Transporter) {}
async sendWelcomeEmail(email: string): Promise<void> {
await this.transporter.sendMail({
to: email,
subject: 'Welcome',
html: '<h1>Welcome!</h1>'
});
}
}
class User {
constructor(
private email: string,
private emailService: EmailService
) {}
async sendWelcomeEmail() {
await this.emailService.sendWelcomeEmail(this.email);
}
}
// Test after refactoring
describe('EmailService', () => {
it('should send welcome email', async () => {
const mockTransport = {
sendMail: jest.fn().mockResolvedValue({ messageId: '123' })
};
const emailService = new EmailService(mockTransport);
await emailService.sendWelcomeEmail('test@example.com');
expect(mockTransport.sendMail).toHaveBeenCalledWith(
expect.objectContaining({
to: 'test@example.com',
subject: 'Welcome'
})
);
});
});
describe('User', () => {
it('should delegate welcome email to email service', async () => {
const mockEmailService = {
sendWelcomeEmail: jest.fn().mockResolvedValue(undefined)
};
const user = new User('test@example.com', mockEmailService);
await user.sendWelcomeEmail();
expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith('test@example.com');
});
});Replace Conditional with Polymorphism
// Before: Switch statement
class PaymentProcessor {
processPayment(payment: Payment): PaymentResult {
switch (payment.type) {
case 'credit_card':
return this.processCreditCard(payment);
case 'paypal':
return this.processPayPal(payment);
case 'bank_transfer':
return this.processBankTransfer(payment);
default:
throw new Error('Unknown payment type');
}
}
}
// Test before refactoring
describe('PaymentProcessor', () => {
it('should process credit card payment', () => {
const processor = new PaymentProcessor();
const payment = { type: 'credit_card', amount: 100, cardNumber: '1234' };
const result = processor.processPayment(payment);
expect(result.status).toBe('success');
});
it('should process PayPal payment', () => {
const processor = new PaymentProcessor();
const payment = { type: 'paypal', amount: 100, email: 'user@example.com' };
const result = processor.processPayment(payment);
expect(result.status).toBe('success');
});
});
// After refactoring: Polymorphic payment methods
interface PaymentMethod {
process(amount: number): Promise<PaymentResult>;
}
class CreditCardPayment implements PaymentMethod {
constructor(private cardNumber: string, private cvv: string) {}
async process(amount: number): Promise<PaymentResult> {
// Process credit card
return { status: 'success', transactionId: '123' };
}
}
class PayPalPayment implements PaymentMethod {
constructor(private email: string, private token: string) {}
async process(amount: number): Promise<PaymentResult> {
// Process PayPal
return { status: 'success', transactionId: '456' };
}
}
class PaymentProcessor {
async processPayment(paymentMethod: PaymentMethod, amount: number): Promise<PaymentResult> {
return await paymentMethod.process(amount);
}
}
// Test after refactoring - test each payment method independently
describe('CreditCardPayment', () => {
it('should process payment', async () => {
const payment = new CreditCardPayment('1234', '123');
const result = await payment.process(100);
expect(result.status).toBe('success');
});
});
describe('PayPalPayment', () => {
it('should process payment', async () => {
const payment = new PayPalPayment('user@example.com', 'token123');
const result = await payment.process(100);
expect(result.status).toBe('success');
});
});
describe('PaymentProcessor', () => {
it('should delegate to payment method', async () => {
const mockPaymentMethod = {
process: jest.fn().mockResolvedValue({ status: 'success' })
};
const processor = new PaymentProcessor();
await processor.processPayment(mockPaymentMethod, 100);
expect(mockPaymentMethod.process).toHaveBeenCalledWith(100);
});
});Testing Legacy Code
Characterization Tests
When refactoring code without tests, write characterization tests that document current behavior:
// Legacy code with no tests
function calculateDiscount(customer: any, orderTotal: number): number {
let discount = 0;
if (customer.type === 'gold') {
if (orderTotal > 1000) {
discount = orderTotal * 0.15;
} else {
discount = orderTotal * 0.10;
}
} else if (customer.type === 'silver') {
if (orderTotal > 500) {
discount = orderTotal * 0.08;
} else {
discount = orderTotal * 0.05;
}
} else {
if (orderTotal > 100) {
discount = orderTotal * 0.02;
}
}
if (customer.loyaltyYears > 5) {
discount = discount * 1.1;
}
return Math.min(discount, orderTotal * 0.5);
}
// Step 1: Write characterization tests documenting behavior
describe('calculateDiscount - characterization tests', () => {
describe('gold customers', () => {
it('should give 15% discount for orders over $1000', () => {
const customer = { type: 'gold', loyaltyYears: 0 };
expect(calculateDiscount(customer, 1500)).toBe(225); // 15%
});
it('should give 10% discount for orders under $1000', () => {
const customer = { type: 'gold', loyaltyYears: 0 };
expect(calculateDiscount(customer, 500)).toBe(50); // 10%
});
});
describe('silver customers', () => {
it('should give 8% discount for orders over $500', () => {
const customer = { type: 'silver', loyaltyYears: 0 };
expect(calculateDiscount(customer, 1000)).toBe(80); // 8%
});
it('should give 5% discount for orders under $500', () => {
const customer = { type: 'silver', loyaltyYears: 0 };
expect(calculateDiscount(customer, 300)).toBe(15); // 5%
});
});
describe('regular customers', () => {
it('should give 2% discount for orders over $100', () => {
const customer = { type: 'regular', loyaltyYears: 0 };
expect(calculateDiscount(customer, 500)).toBe(10); // 2%
});
it('should give no discount for orders under $100', () => {
const customer = { type: 'regular', loyaltyYears: 0 };
expect(calculateDiscount(customer, 50)).toBe(0);
});
});
describe('loyalty bonus', () => {
it('should increase discount by 10% for customers with 5+ years', () => {
const customer = { type: 'gold', loyaltyYears: 6 };
expect(calculateDiscount(customer, 1000)).toBe(110); // 100 * 1.1
});
});
describe('maximum discount', () => {
it('should cap discount at 50% of order total', () => {
const customer = { type: 'gold', loyaltyYears: 10 };
expect(calculateDiscount(customer, 1000)).toBe(500); // Capped at 50%
});
});
});
// Step 2: Refactor with confidence
class DiscountCalculator {
private readonly GOLD_HIGH_DISCOUNT = 0.15;
private readonly GOLD_LOW_DISCOUNT = 0.10;
private readonly GOLD_THRESHOLD = 1000;
private readonly SILVER_HIGH_DISCOUNT = 0.08;
private readonly SILVER_LOW_DISCOUNT = 0.05;
private readonly SILVER_THRESHOLD = 500;
private readonly REGULAR_DISCOUNT = 0.02;
private readonly REGULAR_THRESHOLD = 100;
private readonly LOYALTY_BONUS = 1.1;
private readonly LOYALTY_YEARS_THRESHOLD = 5;
private readonly MAX_DISCOUNT_RATE = 0.5;
calculate(customer: Customer, orderTotal: number): number {
const baseDiscount = this.calculateBaseDiscount(customer, orderTotal);
const withLoyaltyBonus = this.applyLoyaltyBonus(baseDiscount, customer);
return this.capDiscount(withLoyaltyBonus, orderTotal);
}
private calculateBaseDiscount(customer: Customer, orderTotal: number): number {
switch (customer.type) {
case 'gold':
return this.calculateGoldDiscount(orderTotal);
case 'silver':
return this.calculateSilverDiscount(orderTotal);
default:
return this.calculateRegularDiscount(orderTotal);
}
}
private calculateGoldDiscount(orderTotal: number): number {
const rate = orderTotal > this.GOLD_THRESHOLD
? this.GOLD_HIGH_DISCOUNT
: this.GOLD_LOW_DISCOUNT;
return orderTotal * rate;
}
private calculateSilverDiscount(orderTotal: number): number {
const rate = orderTotal > this.SILVER_THRESHOLD
? this.SILVER_HIGH_DISCOUNT
: this.SILVER_LOW_DISCOUNT;
return orderTotal * rate;
}
private calculateRegularDiscount(orderTotal: number): number {
return orderTotal > this.REGULAR_THRESHOLD
? orderTotal * this.REGULAR_DISCOUNT
: 0;
}
private applyLoyaltyBonus(discount: number, customer: Customer): number {
return customer.loyaltyYears > this.LOYALTY_YEARS_THRESHOLD
? discount * this.LOYALTY_BONUS
: discount;
}
private capDiscount(discount: number, orderTotal: number): number {
return Math.min(discount, orderTotal * this.MAX_DISCOUNT_RATE);
}
}
// All characterization tests should still pass!Approval Testing
For complex outputs, use approval testing (golden master testing):
import { verify } from 'approvals';
describe('ReportGenerator', () => {
it('should generate report with correct format', () => {
const generator = new ReportGenerator();
const data = {
customers: [...],
orders: [...],
revenue: 10000
};
const report = generator.generate(data);
// Approve the output - creates .approved file on first run
verify(report);
// Future runs compare against approved file
// If different, test fails and shows diff
});
});Mutation Testing
Verify test quality by introducing mutations:
// Install: npm install --save-dev @stryker-mutator/core
// stryker.conf.json
{
"mutator": "typescript",
"packageManager": "npm",
"testRunner": "jest",
"coverageAnalysis": "perTest",
"mutate": [
"src/**/*.ts",
"!src/**/*.spec.ts"
]
}
// Run: npx stryker run
// Example mutation results:
// Original: if (price > 100)
// Mutant 1: if (price >= 100) - KILLED by test
// Mutant 2: if (price < 100) - KILLED by test
// Mutant 3: if (true) - SURVIVED (weak test!)Best Practices
1. Red-Green-Refactor Cycle
- Write test (red)
- Make it pass (green)
- Refactor (tests stay green)
2. Test One Thing at a Time
- Each test should verify one behavior
- Clear, focused assertions
3. Use Test Doubles Appropriately
- Mock external dependencies
- Use real objects for value objects
- Avoid mocking what you don't own
4. Keep Tests Fast
- Unit tests < 1ms
- Integration tests < 100ms
- Full suite < 10 minutes
5. Maintain Test Quality
- Refactor tests too
- Remove duplicate test code
- Keep tests readable
This comprehensive testing approach ensures safe, confident refactoring with minimal risk of introducing bugs.
Tools and Automation
Static Analysis Tools
- JavaScript/TypeScript: ESLint, TSLint, SonarQube
- Python: Pylint, Flake8, Black
- Java: SonarQube, PMD, Checkstyle, SpotBugs
- C#: ReSharper, SonarQube
Refactoring Tools
- IDE Support: Visual Studio Code, IntelliJ IDEA, WebStorm
- Automated Refactoring: Language-specific refactoring tools
- Code Review: GitHub, GitLab, Bitbucket
Metrics to Track
- Cyclomatic Complexity: Keep below 10
- Code Coverage: Aim for >80%
- Code Duplication: Minimize duplicated blocks
- Method Length: Keep methods under 20 lines
- Class Size: Keep classes under 300 lines
Related skills
FAQ
Must tests exist before refactoring?
The skill requires adequate coverage first and instructs writing tests if they are missing.
Can I apply multiple patterns at once?
It recommends one small change per iteration with tests run immediately after each step.
Does it cover framework-specific UI refactors?
Patterns are general; pair with frontend-coding or frontend-code-review for UI-heavy refactors.