跳到主要内容

策略模式

问题

什么是策略模式?如何用策略模式优化大量的 if-else 或 switch-case 代码?前端有哪些典型应用?

答案

策略模式(Strategy Pattern)定义一系列算法,将每个算法封装起来,并使它们可以相互替换。策略模式让算法独立于使用它的客户端而变化。


核心概念

组成部分

角色说明
Context上下文,持有策略引用
Strategy策略接口,定义算法标准
ConcreteStrategy具体策略,实现具体算法

基础实现

传统实现 vs 策略模式

// ❌ 传统实现 - if-else 地狱
function calculatePrice(type: string, price: number): number {
if (type === 'normal') {
return price;
} else if (type === 'vip') {
return price * 0.9;
} else if (type === 'svip') {
return price * 0.8;
} else if (type === 'newUser') {
return price * 0.7;
} else if (type === 'doubleEleven') {
return price * 0.5;
}
return price;
}

// ✅ 策略模式重构
interface PriceStrategy {
calculate(price: number): number;
}

class NormalStrategy implements PriceStrategy {
calculate(price: number) {
return price;
}
}

class VipStrategy implements PriceStrategy {
calculate(price: number) {
return price * 0.9;
}
}

class SvipStrategy implements PriceStrategy {
calculate(price: number) {
return price * 0.8;
}
}

class NewUserStrategy implements PriceStrategy {
calculate(price: number) {
return price * 0.7;
}
}

// 上下文
class PriceContext {
private strategy: PriceStrategy;

constructor(strategy: PriceStrategy) {
this.strategy = strategy;
}

setStrategy(strategy: PriceStrategy) {
this.strategy = strategy;
}

getPrice(price: number): number {
return this.strategy.calculate(price);
}
}

// 使用
const context = new PriceContext(new VipStrategy());
console.log(context.getPrice(100)); // 90

context.setStrategy(new SvipStrategy());
console.log(context.getPrice(100)); // 80

函数式策略(推荐)

// 更简洁的函数式实现
type PriceStrategy = (price: number) => number;

const strategies: Record<string, PriceStrategy> = {
normal: (price) => price,
vip: (price) => price * 0.9,
svip: (price) => price * 0.8,
newUser: (price) => price * 0.7,
doubleEleven: (price) => price * 0.5,
};

function calculatePrice(type: string, price: number): number {
const strategy = strategies[type] || strategies.normal;
return strategy(price);
}

// 使用
console.log(calculatePrice('vip', 100)); // 90
console.log(calculatePrice('svip', 100)); // 80

// 动态添加新策略
strategies.newYear = (price) => price * 0.6;

前端实际应用

1. 表单验证策略

// 验证策略
type Validator = (value: string) => string | null;

const validators: Record<string, Validator> = {
required: (value) => (value.trim() ? null : '此字段必填'),

email: (value) => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(value) ? null : '请输入有效的邮箱地址';
},

phone: (value) => {
const regex = /^1[3-9]\d{9}$/;
return regex.test(value) ? null : '请输入有效的手机号';
},

minLength: (min: number) => (value: string) =>
value.length >= min ? null : `最少输入 ${min} 个字符`,

maxLength: (max: number) => (value: string) =>
value.length <= max ? null : `最多输入 ${max} 个字符`,

pattern: (regex: RegExp, message: string) => (value: string) =>
regex.test(value) ? null : message,
};

// 验证器工厂
function createValidator(rules: Array<{ type: string; params?: unknown[] }>) {
return (value: string): string[] => {
const errors: string[] = [];

for (const rule of rules) {
let validator = validators[rule.type];

// 处理带参数的验证器
if (typeof validator === 'function' && rule.params) {
validator = (validator as Function)(...rule.params);
}

const error = validator?.(value);
if (error) errors.push(error);
}

return errors;
};
}

// 使用
const validateEmail = createValidator([
{ type: 'required' },
{ type: 'email' },
]);

console.log(validateEmail('')); // ['此字段必填']
console.log(validateEmail('invalid')); // ['请输入有效的邮箱地址']
console.log(validateEmail('test@example.com')); // []

2. 支付方式策略

interface PaymentResult {
success: boolean;
orderId: string;
message: string;
}

interface PaymentStrategy {
pay(amount: number): Promise<PaymentResult>;
getName(): string;
}

class AlipayStrategy implements PaymentStrategy {
async pay(amount: number): Promise<PaymentResult> {
console.log(`支付宝支付 ${amount}`);
// 调用支付宝 SDK
return {
success: true,
orderId: `ALI_${Date.now()}`,
message: '支付宝支付成功',
};
}

getName() {
return '支付宝';
}
}

class WechatPayStrategy implements PaymentStrategy {
async pay(amount: number): Promise<PaymentResult> {
console.log(`微信支付 ${amount}`);
// 调用微信支付 SDK
return {
success: true,
orderId: `WX_${Date.now()}`,
message: '微信支付成功',
};
}

getName() {
return '微信支付';
}
}

class CreditCardStrategy implements PaymentStrategy {
private cardNumber: string;

constructor(cardNumber: string) {
this.cardNumber = cardNumber;
}

async pay(amount: number): Promise<PaymentResult> {
console.log(`信用卡 ${this.cardNumber} 支付 ${amount}`);
return {
success: true,
orderId: `CARD_${Date.now()}`,
message: '信用卡支付成功',
};
}

getName() {
return `信用卡 (${this.cardNumber.slice(-4)})`;
}
}

// 支付上下文
class PaymentContext {
private strategy: PaymentStrategy;

constructor(strategy: PaymentStrategy) {
this.strategy = strategy;
}

setStrategy(strategy: PaymentStrategy) {
this.strategy = strategy;
}

async checkout(amount: number) {
console.log(`使用 ${this.strategy.getName()} 支付`);
return this.strategy.pay(amount);
}
}

// 使用
const payment = new PaymentContext(new AlipayStrategy());
await payment.checkout(100);

payment.setStrategy(new WechatPayStrategy());
await payment.checkout(200);

3. 数据格式化策略

type Formatter<T> = (data: T) => string;

// 日期格式化策略
const dateFormatters: Record<string, Formatter<Date>> = {
simple: (date) => date.toLocaleDateString('zh-CN'),

full: (date) =>
date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
}),

relative: (date) => {
const diff = Date.now() - date.getTime();
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);

if (days > 0) return `${days} 天前`;
if (hours > 0) return `${hours} 小时前`;
if (minutes > 0) return `${minutes} 分钟前`;
return '刚刚';
},

iso: (date) => date.toISOString(),
};

function formatDate(date: Date, format: keyof typeof dateFormatters = 'simple') {
return dateFormatters[format](date);
}

// 使用
const now = new Date();
console.log(formatDate(now, 'simple')); // 2024/1/15
console.log(formatDate(now, 'full')); // 2024/01/15 14:30:00
console.log(formatDate(now, 'relative')); // 刚刚

// 金额格式化策略
const currencyFormatters: Record<string, Formatter<number>> = {
CNY: (amount) => `¥${amount.toFixed(2)}`,
USD: (amount) => `$${amount.toFixed(2)}`,
EUR: (amount) => `${amount.toFixed(2)}`,
compact: (amount) => {
if (amount >= 10000) return `${(amount / 10000).toFixed(1)}`;
if (amount >= 1000) return `${(amount / 1000).toFixed(1)}`;
return amount.toString();
},
};

function formatCurrency(
amount: number,
type: keyof typeof currencyFormatters = 'CNY'
) {
return currencyFormatters[type](amount);
}

console.log(formatCurrency(1234.5)); // ¥1234.50
console.log(formatCurrency(12345, 'compact')); // 1.2万

4. 排序策略

interface Product {
id: number;
name: string;
price: number;
sales: number;
rating: number;
createdAt: Date;
}

type SortStrategy<T> = (a: T, b: T) => number;

const productSortStrategies: Record<string, SortStrategy<Product>> = {
priceAsc: (a, b) => a.price - b.price,
priceDesc: (a, b) => b.price - a.price,
salesDesc: (a, b) => b.sales - a.sales,
ratingDesc: (a, b) => b.rating - a.rating,
newest: (a, b) => b.createdAt.getTime() - a.createdAt.getTime(),
nameAsc: (a, b) => a.name.localeCompare(b.name, 'zh-CN'),
};

function sortProducts(
products: Product[],
strategy: keyof typeof productSortStrategies
): Product[] {
const sortFn = productSortStrategies[strategy];
return [...products].sort(sortFn);
}

// React 组件中使用
function ProductList() {
const [sortType, setSortType] = useState<string>('salesDesc');
const [products, setProducts] = useState<Product[]>([]);

const sortedProducts = useMemo(
() => sortProducts(products, sortType as keyof typeof productSortStrategies),
[products, sortType]
);

return (
<div>
<select value={sortType} onChange={(e) => setSortType(e.target.value)}>
<option value="salesDesc">销量优先</option>
<option value="priceAsc">价格从低到高</option>
<option value="priceDesc">价格从高到低</option>
<option value="ratingDesc">评分优先</option>
<option value="newest">最新上架</option>
</select>

{sortedProducts.map((product) => (
<div key={product.id}>{product.name}</div>
))}
</div>
);
}

5. 动画策略(缓动函数)

// 缓动函数策略
type EasingFunction = (t: number) => number;

const easingStrategies: Record<string, EasingFunction> = {
linear: (t) => t,

easeInQuad: (t) => t * t,

easeOutQuad: (t) => t * (2 - t),

easeInOutQuad: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),

easeInCubic: (t) => t * t * t,

easeOutCubic: (t) => --t * t * t + 1,

easeInOutCubic: (t) =>
t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,

easeOutElastic: (t) => {
const p = 0.3;
return Math.pow(2, -10 * t) * Math.sin(((t - p / 4) * (2 * Math.PI)) / p) + 1;
},

easeOutBounce: (t) => {
if (t < 1 / 2.75) {
return 7.5625 * t * t;
} else if (t < 2 / 2.75) {
return 7.5625 * (t -= 1.5 / 2.75) * t + 0.75;
} else if (t < 2.5 / 2.75) {
return 7.5625 * (t -= 2.25 / 2.75) * t + 0.9375;
} else {
return 7.5625 * (t -= 2.625 / 2.75) * t + 0.984375;
}
},
};

// 动画工具
function animate(options: {
from: number;
to: number;
duration: number;
easing?: keyof typeof easingStrategies;
onUpdate: (value: number) => void;
onComplete?: () => void;
}) {
const {
from,
to,
duration,
easing = 'easeOutQuad',
onUpdate,
onComplete,
} = options;

const easingFn = easingStrategies[easing];
const startTime = performance.now();

function tick(currentTime: number) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const easedProgress = easingFn(progress);
const currentValue = from + (to - from) * easedProgress;

onUpdate(currentValue);

if (progress < 1) {
requestAnimationFrame(tick);
} else {
onComplete?.();
}
}

requestAnimationFrame(tick);
}

// 使用
const element = document.getElementById('box')!;
animate({
from: 0,
to: 300,
duration: 1000,
easing: 'easeOutBounce',
onUpdate: (value) => {
element.style.transform = `translateX(${value}px)`;
},
onComplete: () => console.log('Animation complete'),
});

6. 压缩策略

interface CompressionStrategy {
compress(data: string): string;
decompress(data: string): string;
getName(): string;
}

// Base64 策略(实际不是压缩,仅示例)
class Base64Strategy implements CompressionStrategy {
compress(data: string) {
return btoa(data);
}

decompress(data: string) {
return atob(data);
}

getName() {
return 'Base64';
}
}

// LZString 策略(需要 lz-string 库)
class LZStringStrategy implements CompressionStrategy {
compress(data: string) {
// 模拟 LZString.compress
return data;
}

decompress(data: string) {
// 模拟 LZString.decompress
return data;
}

getName() {
return 'LZString';
}
}

// 存储上下文
class Storage {
private strategy: CompressionStrategy;
private storage: globalThis.Storage;

constructor(strategy: CompressionStrategy, storage = localStorage) {
this.strategy = strategy;
this.storage = storage;
}

setItem(key: string, value: unknown) {
const serialized = JSON.stringify(value);
const compressed = this.strategy.compress(serialized);
this.storage.setItem(key, compressed);
}

getItem<T>(key: string): T | null {
const compressed = this.storage.getItem(key);
if (!compressed) return null;

const serialized = this.strategy.decompress(compressed);
return JSON.parse(serialized);
}
}

// 使用
const storage = new Storage(new Base64Strategy());
storage.setItem('user', { name: 'John', age: 30 });

常见面试问题

Q1: 策略模式的优缺点?

答案

优点缺点
消除大量条件判断增加策略类数量
符合开闭原则客户端需要知道所有策略
算法可复用策略间不能通信
运行时动态切换-

Q2: 策略模式与工厂模式的区别?

答案

对比项策略模式工厂模式
目的封装算法封装创建
关注点行为对象
结果执行策略返回对象
使用方式替换算法创建实例
// 策略模式 - 关注算法执行
const strategy = strategies[type];
strategy.execute(data);

// 工厂模式 - 关注对象创建
const product = Factory.create(type);
product.doSomething();

Q3: 如何用策略模式重构 if-else?

答案

// ❌ 原始代码
function getDiscount(level: string, price: number) {
if (level === 'bronze') return price * 0.95;
if (level === 'silver') return price * 0.9;
if (level === 'gold') return price * 0.85;
if (level === 'platinum') return price * 0.8;
return price;
}

// ✅ 策略模式重构
const discountStrategies: Record<string, (price: number) => number> = {
bronze: (price) => price * 0.95,
silver: (price) => price * 0.9,
gold: (price) => price * 0.85,
platinum: (price) => price * 0.8,
default: (price) => price,
};

function getDiscount(level: string, price: number) {
const strategy = discountStrategies[level] || discountStrategies.default;
return strategy(price);
}

// 好处:
// 1. 新增等级只需添加策略,不改原函数
// 2. 策略可单独测试
// 3. 策略可复用

Q4: 策略模式在 React 中的应用?

答案

// 1. 条件渲染策略
const renderStrategies = {
loading: () => <Spinner />,
error: (error: Error) => <ErrorMessage error={error} />,
empty: () => <EmptyState />,
success: (data: Data[]) => <DataList data={data} />,
};

function DataContainer({ status, data, error }: Props) {
const render = renderStrategies[status];
return render?.(status === 'error' ? error : data);
}

// 2. 表单处理策略
const submitStrategies = {
create: (data: FormData) => api.create(data),
update: (data: FormData) => api.update(data.id, data),
draft: (data: FormData) => api.saveDraft(data),
};

function Form({ mode }: { mode: 'create' | 'update' | 'draft' }) {
const handleSubmit = (data: FormData) => {
const strategy = submitStrategies[mode];
return strategy(data);
};
// ...
}

// 3. Hook 中使用策略
function useStorage(type: 'local' | 'session') {
const strategies = {
local: localStorage,
session: sessionStorage,
};

const storage = strategies[type];

return {
get: (key: string) => JSON.parse(storage.getItem(key) || 'null'),
set: (key: string, value: unknown) => storage.setItem(key, JSON.stringify(value)),
};
}

相关链接