When to Use Design Patterns for Refactoring
Refactoring with design patterns is beneficial when legacy code becomes challenging to maintain or extend. Situations that may call for design patterns include:
- Redundant Code: When multiple sections of code share similar logic, design patterns help consolidate functionality.
- Tight Coupling: Patterns can decouple code to improve modularity and make components more reusable.
- Frequent Changes: If requirements change often, design patterns provide flexibility, making it easier to adapt code.
Using the Factory Pattern for Object Creation
Legacy code often includes hard-coded object instantiations, making it difficult to introduce new object types. The Factory pattern encapsulates object creation, providing a flexible way to manage multiple types:
Example: Refactoring with the Factory Pattern
// Legacy Code
function createProduct(type) {
if (type === "Electronics") return new Electronics();
if (type === "Furniture") return new Furniture();
}
// Refactored Code with Factory Pattern
class ProductFactory {
static createProduct(type) {
switch (type) {
case "Electronics": return new Electronics();
case "Furniture": return new Furniture();
default: throw new Error("Invalid product type");
}
}
}
With the Factory pattern, the product creation logic is centralized, making it easy to add new types without modifying the core code. This pattern reduces redundancy and enhances scalability.
Implementing Strategy Pattern for Conditional Logic
Legacy code may have complex `if-else` or `switch` statements, which can be refactored with the Strategy pattern to improve readability and flexibility. The Strategy pattern encapsulates algorithms, allowing the code to switch strategies at runtime.
Example: Refactoring with the Strategy Pattern
// Legacy Code
function calculateDiscount(type, amount) {
if (type === "student") return amount * 0.8;
if (type === "senior") return amount * 0.9;
return amount;
}
// Refactored Code with Strategy Pattern
class DiscountStrategy {
calculate(amount) {
return amount;
}
}
class StudentDiscount extends DiscountStrategy {
calculate(amount) {
return amount * 0.8;
}
}
class SeniorDiscount extends DiscountStrategy {
calculate(amount) {
return amount * 0.9;
}
}
// Usage
const discount = new StudentDiscount();
console.log(discount.calculate(100)); // Output: 80
Using the Strategy pattern here makes it easy to add new discount types without modifying the main logic. This pattern makes the code modular, extensible, and more maintainable.
Adding the Decorator Pattern for Extending Functionality
The Decorator pattern is useful for adding additional functionality to classes without altering their structure. This is ideal for refactoring legacy code that has grown due to added features or options.
Example: Refactoring with the Decorator Pattern
// Legacy Code
class Notification {
send(message) {
console.log(`Sending message: ${message}`);
}
}
// Refactored Code with Decorator Pattern
class NotificationDecorator {
constructor(notification) {
this.notification = notification;
}
send(message) {
this.notification.send(message);
}
}
class EmailNotification extends NotificationDecorator {
send(message) {
super.send(message);
console.log(`Sending email: ${message}`);
}
}
class SMSNotification extends NotificationDecorator {
send(message) {
super.send(message);
console.log(`Sending SMS: ${message}`);
}
}
// Usage
const notification = new EmailNotification(new Notification());
notification.send("Hello, world!");
// Output:
// Sending message: Hello, world!
// Sending email: Hello, world!
The Decorator pattern allows additional functionalities, such as email or SMS notifications, to be added dynamically without modifying the core `Notification` class. This approach improves flexibility and reduces code duplication.
Using Template Method Pattern for Common Logic
When multiple methods share similar logic with slight variations, the Template Method pattern provides a structure for refactoring. This pattern defines the skeleton of an operation, allowing subclasses to implement specific steps.
Example: Refactoring with Template Method Pattern
class DocumentProcessor {
processDocument() {
this.open();
this.parseContent();
this.close();
}
open() {
console.log("Opening document...");
}
close() {
console.log("Closing document...");
}
parseContent() {
throw new Error("Method not implemented");
}
}
class PDFProcessor extends DocumentProcessor {
parseContent() {
console.log("Parsing PDF content...");
}
}
class WordProcessor extends DocumentProcessor {
parseContent() {
console.log("Parsing Word content...");
}
}
// Usage
const pdfProcessor = new PDFProcessor();
pdfProcessor.processDocument();
// Output:
// Opening document...
// Parsing PDF content...
// Closing document...
The Template Method pattern refactors common functionality into a base class, allowing subclasses to implement their specific parsing logic. This approach reduces code duplication and ensures consistency across subclasses.
Best Practices for Refactoring with Design Patterns
- Identify Reusable Code: Look for redundant code blocks that could benefit from centralized patterns like Factory or Strategy.
- Favor Composition over Inheritance: Patterns like Decorator allow for flexible, dynamic extensions without modifying existing code.
- Refactor Incrementally: Introduce design patterns gradually to prevent over-complicating the code and ensure that each refactor adds value.
Conclusion
Refactoring with design patterns enhances code quality, making legacy code more modular, flexible, and maintainable. Patterns like Factory, Strategy, Decorator, and Template Method address specific challenges, offering structured solutions that improve readability, scalability, and adaptability. By applying design patterns during refactoring, developers can transform legacy code into a robust foundation that supports future growth and change.