Strategy Pattern for Clean Conditionals
Replace complex if/else chains and switch statements with the Strategy pattern for more maintainable and extensible code.
Long if/else chains and switch statements are one of the most common code smells. The Strategy pattern replaces them with interchangeable algorithms, making code open for extension without modification.
The Problem
// ❌ Every new pricing rule means modifying this function
function calculatePrice(product: Product, userType: string): number {
let price = product.basePrice;
if (userType === 'premium') {
price *= 0.8; // 20% discount
if (product.category === 'electronics') {
price *= 0.95; // Extra 5% on electronics
}
} else if (userType === 'employee') {
price *= 0.7; // 30% discount
} else if (userType === 'wholesale') {
if (product.quantity > 100) {
price *= 0.6;
} else if (product.quantity > 50) {
price *= 0.75;
} else {
price *= 0.85;
}
}
// This grows forever...
return price;
}
The Strategy Solution
// Define the strategy interface
interface PricingStrategy {
calculate(product: Product): number;
}
// Each strategy encapsulates its own logic
class StandardPricing implements PricingStrategy {
calculate(product: Product): number {
return product.basePrice;
}
}
class PremiumPricing implements PricingStrategy {
calculate(product: Product): number {
let price = product.basePrice * 0.8;
if (product.category === 'electronics') {
price *= 0.95;
}
return price;
}
}
class EmployeePricing implements PricingStrategy {
calculate(product: Product): number {
return product.basePrice * 0.7;
}
}
class WholesalePricing implements PricingStrategy {
calculate(product: Product): number {
const { basePrice, quantity } = product;
if (quantity > 100) return basePrice * 0.6;
if (quantity > 50) return basePrice * 0.75;
return basePrice * 0.85;
}
}
// Strategy registry
class PricingService {
private strategies = new Map<string, PricingStrategy>();
constructor() {
this.strategies.set('standard', new StandardPricing());
this.strategies.set('premium', new PremiumPricing());
this.strategies.set('employee', new EmployeePricing());
this.strategies.set('wholesale', new WholesalePricing());
}
register(name: string, strategy: PricingStrategy): void {
this.strategies.set(name, strategy);
}
calculate(product: Product, userType: string): number {
const strategy = this.strategies.get(userType) ?? this.strategies.get('standard')!;
return strategy.calculate(product);
}
}
Functional Strategy Pattern
In TypeScript, you don’t need classes for simple strategies. Functions work beautifully:
type SortStrategy<T> = (items: T[]) => T[];
const sortByDate: SortStrategy<Article> = (items) =>
[...items].sort((a, b) => b.date.getTime() - a.date.getTime());
const sortByTitle: SortStrategy<Article> = (items) =>
[...items].sort((a, b) => a.title.localeCompare(b.title));
const sortByPopularity: SortStrategy<Article> = (items) =>
[...items].sort((a, b) => b.views - a.views);
// Usage
function displayArticles(articles: Article[], sort: SortStrategy<Article>) {
const sorted = sort(articles);
sorted.forEach(article => render(article));
}
displayArticles(articles, sortByPopularity);
Composable Strategies
Strategies can be combined for powerful flexibility:
interface ValidationStrategy {
validate(value: string): ValidationResult;
}
const required: ValidationStrategy = {
validate: (value) =>
value.trim().length > 0
? { valid: true }
: { valid: false, error: 'Field is required' },
};
const minLength = (min: number): ValidationStrategy => ({
validate: (value) =>
value.length >= min
? { valid: true }
: { valid: false, error: `Must be at least ${min} characters` },
});
const matchesPattern = (pattern: RegExp, msg: string): ValidationStrategy => ({
validate: (value) =>
pattern.test(value)
? { valid: true }
: { valid: false, error: msg },
});
// Compose strategies
function composeValidators(...strategies: ValidationStrategy[]): ValidationStrategy {
return {
validate(value: string): ValidationResult {
for (const strategy of strategies) {
const result = strategy.validate(value);
if (!result.valid) return result;
}
return { valid: true };
},
};
}
// Build complex validators from simple ones
const emailValidator = composeValidators(
required,
minLength(5),
matchesPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, 'Invalid email format'),
);
const passwordValidator = composeValidators(
required,
minLength(8),
matchesPattern(/[A-Z]/, 'Must contain an uppercase letter'),
matchesPattern(/[0-9]/, 'Must contain a number'),
);
The Strategy pattern turns rigid conditional logic into a collection of pluggable, testable, reusable algorithms.
“Define a family of algorithms, encapsulate each one, and make them interchangeable.” — Gang of Four