Smart Code Refactoring with AI
Improve your code quality with artificial intelligence
What is Refactoring?
Refactoring is improving the internal structure of code without changing its external behavior. AI can identify code smells, suggest design patterns, and apply SOLID principles automatically.
Principles AI Can Apply
SOLID
- Single Responsibility - One class, one responsibility
- Open/Closed - Open for extension, closed for modification
- Liskov Substitution - Subtypes must be substitutable
- Interface Segregation - Specific interfaces
- Dependency Inversion - Depend on abstractions
Refactoring Prompts
General refactoring
"Refactor this code following SOLID and clean code principles:
[code]
Keep the same functionality but improve:
- Readability
- Maintainability
- Testability
- Separation of concerns
Explain each change you make."
Extract functions
"This function is too long (150 lines). Break it into smaller functions with descriptive names:
[code]
Each function should have a single responsibility and a maximum of 20 lines."
Apply design patterns
"This code has a lot of nested if/else. What design pattern can I apply to improve it?
[code]
Implement the suggested pattern and explain why it's better."
Practical Examples
Before: Coupled code
class UserService {
async register(user) {
// Validate
if (!user.email) throw new Error('Email required');
if (!user.password) throw new Error('Password required');
// Save to DB
const db = new Database();
await db.save(user);
// Send email
const mailer = new Mailer();
await mailer.send(user.email, 'Welcome!');
// Log
console.log(`User ${user.email} registered`);
}
}
After: Refactored with AI
class UserValidator {
validate(user) {
if (!user.email) throw new Error('Email required');
if (!user.password) throw new Error('Password required');
}
}
class UserService {
constructor(validator, repository, notifier, logger) {
this.validator = validator;
this.repository = repository;
this.notifier = notifier;
this.logger = logger;
}
async register(user) {
this.validator.validate(user);
await this.repository.save(user);
await this.notifier.sendWelcome(user.email);
this.logger.log(`User ${user.email} registered`);
}
}
Code Smells AI Detects
- Long functions: More than 30 lines
- Duplication: Repeated code
- Unclear names: Variables like x, data, temp
- Too many parameters: More than 3-4 arguments
- God classes: One class that does everything
- Coupling: Hardcoded dependencies
- Magic numbers: Numbers without context
Refactoring Workflow
- Make sure you have passing tests
- Ask the AI to analyze the code
- Review suggestions one by one
- Apply changes in small commits
- Run tests after each change
- Commit with a descriptive message
Common Design Patterns
Design patterns are proven solutions to recurring problems in software development. AI can identify when your code would benefit from applying a specific pattern and generate the correct implementation. Here are the most useful ones in modern development.
Strategy Pattern
The Strategy pattern is ideal when you have multiple interchangeable algorithms and want to avoid huge if/else or switch chains. It allows selecting the algorithm at runtime without modifying client code.
// Before: hard-to-maintain if/else chain
function calculateShipping(method, weight) {
if (method === 'standard') return weight * 0.5;
if (method === 'express') return weight * 0.5 + 15;
if (method === 'overnight') return weight * 0.5 + 30;
if (method === 'drone') return weight * 0.3 + 50;
throw new Error('Unknown method');
}
// After: Strategy Pattern
const shippingStrategies = {
standard: (weight) => weight * 0.5,
express: (weight) => weight * 0.5 + 15,
overnight: (weight) => weight * 0.5 + 30,
drone: (weight) => weight * 0.3 + 50,
};
function calculateShipping(method, weight) {
const strategy = shippingStrategies[method];
if (!strategy) throw new Error(`Unknown method: ${method}`);
return strategy(weight);
}
// Prompt for the AI:
"This code has a switch with 12 cases. Refactor it using
the Strategy pattern. Each strategy should be a separate class
that implements a common interface."
Observer Pattern
The Observer pattern allows an object to notify others when its state changes, without observers needing to know the subject's internal details. It's the foundation of event systems in JavaScript.
// Prompt for the AI:
"I have a system where when a user registers, I need to:
1. Send welcome email
2. Create analytics profile
3. Notify the sales team via Slack
4. Add to the newsletter list
Currently everything is in one giant function. Refactor it
using the Observer/EventEmitter pattern so each action
is independent and can be added/removed without modifying
the registration code."
Factory Pattern
The Factory pattern centralizes object creation, allowing client code to not need to know the concrete classes being instantiated. It's especially useful when object creation is complex or depends on external conditions.
// Prompt for the AI:
"I have this code that creates different types of reports
based on the requested format:
[code with new PdfReport(), new ExcelReport(), new CsvReport()]
Refactor it using the Factory pattern. The factory should:
- Accept the report type as a string
- Return the correct instance
- Support registering new types at runtime
- Throw a descriptive error if the type doesn't exist
- Include a Report interface that all implement"
Code Quality Metrics
Code metrics give you objective data about your software's quality. Instead of relying only on intuition, you can use these metrics to identify which parts of the code need priority refactoring and measure the progress of your improvements.
Cyclomatic Complexity
Cyclomatic complexity measures the number of independent paths through your code. Each if, else, switch case, for, while, or catch adds a path. High complexity indicates the function is hard to test and maintain.
// Cyclomatic complexity: 8 (too high)
function processOrder(order) {
if (!order) return null; // +1
if (order.status === 'cancelled') return; // +1
for (const item of order.items) { // +1
if (item.stock < item.qty) { // +1
if (item.backorder) { // +1
notifyBackorder(item);
} else {
throw new Error('Out of stock');
}
}
if (item.discount) { // +1
applyDiscount(item);
}
}
if (order.total > 1000) { // +1
flagForReview(order);
}
return finalize(order); // +1 (base)
}
// Prompt for the AI:
"This function has a cyclomatic complexity of 8.
Reduce it to a maximum of 3 by extracting functions.
Each extracted function should have a maximum complexity of 2."
Coupling and Cohesion
Coupling measures how much one module depends on other modules. Cohesion measures how related the responsibilities are within a single module. The goal is low coupling and high cohesion: each module does one thing well and depends as little as possible on others.
// High coupling (bad): UserService directly depends on
// Database, Mailer, Logger, and PaymentGateway
class UserService {
async register(user) {
const db = new Database(); // direct coupling
const mailer = new Mailer(); // direct coupling
await db.save(user);
await mailer.send(user.email);
}
}
// Low coupling (good): UserService depends on interfaces
class UserService {
constructor(userRepo, notifier) { // dependency injection
this.userRepo = userRepo;
this.notifier = notifier;
}
async register(user) {
await this.userRepo.save(user);
await this.notifier.notifyWelcome(user);
}
}
// Prompt for the AI:
"Analyze the coupling of these classes and suggest how
to reduce it using dependency injection and interfaces:
[code]"
Technical Debt
Technical debt is the implicit cost of quick decisions we make during development. Every shortcut, every hack, every unresolved TODO accumulates and makes code harder to modify in the future. AI can help you quantify and prioritize this debt.
Prompt for evaluating technical debt:
"Analyze this module and classify the problems into:
- CRITICAL: Potential bugs or security issues
- HIGH: Hard-to-maintain code blocking new features
- MEDIUM: Code smells reducing readability
- LOW: Aesthetic or style improvements
For each problem, estimate the refactoring time
and the impact on maintainability. Prioritize by ROI
(impact / effort).
[full module code]"
Practical Refactoring Exercises
The best way to improve your refactoring skills is to practice. These exercises are designed for you to solve first on your own and then use AI to compare your solution with an optimized version.
Exercise 1: Eliminate duplication
Identify the duplicated code in the following example and refactor it to follow the DRY (Don't Repeat Yourself) principle. Then ask the AI to review your solution.
// Code with duplication - refactor this:
function createUser(data) {
const errors = [];
if (!data.name || data.name.length < 2) errors.push('Name too short');
if (!data.email || !data.email.includes('@')) errors.push('Invalid email');
if (errors.length) return { success: false, errors };
return db.users.insert({ ...data, createdAt: new Date() });
}
function createProduct(data) {
const errors = [];
if (!data.name || data.name.length < 2) errors.push('Name too short');
if (!data.price || data.price <= 0) errors.push('Invalid price');
if (errors.length) return { success: false, errors };
return db.products.insert({ ...data, createdAt: new Date() });
}
function createOrder(data) {
const errors = [];
if (!data.userId) errors.push('User required');
if (!data.items || !data.items.length) errors.push('Items required');
if (errors.length) return { success: false, errors };
return db.orders.insert({ ...data, createdAt: new Date() });
}
// Prompt: "Refactor these 3 functions eliminating duplication.
// Use a factory function or a pattern that allows reusing
// the validation and insertion logic."
Exercise 2: Simplify conditionals
Nested conditionals are one of the biggest contributors to code complexity. Practice transforming complex conditional logic into clean, readable code.
// Simplify these conditionals:
function getUserDashboard(user) {
if (user) {
if (user.role === 'admin') {
if (user.verified) {
return { view: 'admin-full', features: allFeatures };
} else {
return { view: 'admin-limited', features: basicFeatures };
}
} else if (user.role === 'editor') {
if (user.verified) {
return { view: 'editor-full', features: editorFeatures };
} else {
return { view: 'editor-limited', features: basicFeatures };
}
} else {
return { view: 'viewer', features: viewerFeatures };
}
} else {
return { view: 'login', features: [] };
}
}
// Prompt: "Refactor this function eliminating nesting.
// Use early returns, lookup tables, or whatever pattern you
// consider most appropriate. The result should be easy to
// extend with new roles in the future."
Frequently Asked Questions
When should I refactor and when should I leave the code as is?
Refactor when the code makes it difficult to add new features, when bugs are frequent in a specific module, or when new developers take too long to understand the code. Don't refactor if the code works correctly, won't change soon, and has good test coverage. Martin Fowler's golden rule applies: "Refactor when adding a new feature is harder than it should be." Never refactor without tests that guarantee behavior doesn't change.
Can AI break my code when refactoring?
Yes, it's possible. AI can introduce subtle bugs when refactoring, especially in code with side effects, concurrency logic, or implicit dependencies. That's why it's essential to have automated tests that verify behavior before and after refactoring. Always run your complete test suite after applying any AI-suggested changes. If you don't have tests, generate tests first (using AI) and refactor afterward.
Is it better to refactor everything at once or little by little?
It's always better to refactor incrementally. Large changes are hard to review, have a higher risk of introducing bugs, and are more complicated to revert if something goes wrong. The recommended technique is the "Boy Scout Rule": leave the code a little better than you found it each time you touch it. Make small, atomic commits, each with a single type of change (rename variables, extract a function, eliminate duplication). This facilitates peer review and rollback if needed.
How do I convince my team that refactoring is worth it?
The most effective arguments are quantifiable ones. Show metrics like how long it takes the team to implement new features (if the code is bad, each feature takes longer), bug frequency in specific modules, or onboarding time for new developers. Use AI to generate a technical debt analysis with time and impact estimates. Propose dedicating 20% of the sprint to continuous refactoring instead of stopping everything for a refactoring sprint, which is usually hard to justify to stakeholders.