Published on

策略模式详解

Authors
  • avatar
    Name
    青雲
    Twitter

策略模式(Strategy Pattern)是一种行为型设计模式,它定义了一系列算法,并将每个算法封装起来,使它们可以互相替换。这种模式使得算法的变化不影响使用算法的客户。策略模式提供了在运行时选择算法的机制,增加了系统的灵活性。

为什么需要策略模式?

在电商平台购物时,准备结账时发现平台提供了三种促销策略供你选择:

  1. 满减策略:订单金额满 100 元减 10 元。
  2. 折扣策略:订单金额享受 8 折优惠。
  3. 返现策略:订单每满 50 元返现 5 元。

每种策略都有其适用的场景和计算公式。作为聪明的消费者(即客户端代码),你希望能够根据自己的订单金额自动选择节省最多钱的策略。

在这个例子中,促销策略(满减、折扣、返现)可以看作不同的算法,根据订单金额选择最优的策略就是策略模式的核心思想。

在软件开发中,某些业务逻辑可能会有多种实现方式。例如在支付系统中,可能会有多种支付方式(如信用卡支付、支付宝支付、微信支付等)。如果将所有支付逻辑都放在同一个类中,不仅会导致代码量冗长,还会让系统变得难以维护和扩展。策略模式通过将每种算法封装到独立的策略类中,可以使得算法的变化不影响使用算法的客户代码,增强了系统的灵活性和可维护性。

基本概念

策略模式包含以下几个主要角色:

  1. 策略接口(Strategy):定义了一个算法族,都实现了某个行为,比如支付。
  2. 具体策略类(Concrete Strategy):实现了策略接口的具体算法。
  3. 上下文(Context):持有一个策略类的引用,客户端通过上下文来调用具体策略实现的算法。

实现示例

假设我们要实现一个支付系统,其中包含多种支付方式,例如信用卡支付、支付宝支付和微信支付。我们可以使用策略模式来实现,使得支付方式的变化不会影响到客户代码。

定义策略接口

// 策略接口,定义支付行为
interface PaymentStrategy {
  pay(amount: number): void
}

实现具体策略类

// 具体策略类:信用卡支付
class CreditCardPayment implements PaymentStrategy {
  pay(amount: number): void {
    console.log(`Paid ${amount} using Credit Card.`)
  }
}

// 具体策略类:支付宝支付
class AlipayPayment implements PaymentStrategy {
  pay(amount: number): void {
    console.log(`Paid ${amount} using Alipay.`)
  }
}

// 具体策略类:微信支付
class WeChatPayment implements PaymentStrategy {
  pay(amount: number): void {
    console.log(`Paid ${amount} using WeChat Pay.`)
  }
}

实现上下文类

// 上下文类,持有一个具体策略的引用
class PaymentContext {
  private strategy: PaymentStrategy

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

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

  executePayment(amount: number): void {
    this.strategy.pay(amount)
  }
}

使用策略模式

// 创建上下文并设置支付策略
const paymentContext = new PaymentContext(new CreditCardPayment())

// 使用信用卡支付
paymentContext.executePayment(100)

// 切换到支付宝支付
paymentContext.setStrategy(new AlipayPayment())
paymentContext.executePayment(200)

// 切换到微信支付
paymentContext.setStrategy(new WeChatPayment())
paymentContext.executePayment(300)

Paid 100 using Credit Card. Paid 200 using Alipay. Paid 300 using WeChat Pay.


# 应用场景

策略模式在前端开发中有着广泛的应用,它为动态切换算法或行为提供了一种灵活的解决方案。

## 表单验证

根据不同的验证规则(如邮箱、手机号、密码强度等)选择不同的验证策略,可以简化表单验证逻辑,使得验证过程更为灵活。

### 示例:表单验证策略

```javascript
// 策略接口
class ValidationStrategy {
  validate(value) {
    throw new Error('This method should be overridden by subclasses')
  }
}

// 具体策略类:邮箱验证
class EmailValidation extends ValidationStrategy {
  validate(value) {
    const regex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/
    return regex.test(value)
  }
}

// 具体策略类:手机号验证
class PhoneValidation extends ValidationStrategy {
  validate(value) {
    const regex = /^\d{10}$/ // 简单的10位数字验证
    return regex.test(value)
  }
}

// 具体策略类:密码强度验证
class PasswordValidation extends ValidationStrategy {
  validate(value) {
    return value.length >= 8 // 简单的长度验证
  }
}

// 上下文类
class Validator {
  constructor(strategy) {
    this.strategy = strategy
  }

  setStrategy(strategy) {
    this.strategy = strategy
  }

  validate(value) {
    return this.strategy.validate(value)
  }
}

// 使用示例
const validator = new Validator(new EmailValidation())

console.log(validator.validate('[email protected]')) // true
console.log(validator.validate('invalid-email')) // false

validator.setStrategy(new PhoneValidation())
console.log(validator.validate('1234567890')) // true
console.log(validator.validate('123-456-7890')) // false

validator.setStrategy(new PasswordValidation())
console.log(validator.validate('strongpass')) // true
console.log(validator.validate('weak')) // false

动画策略

根据场景需求动态选择不同的动画效果和执行方式,可以灵活地实现动画效果的切换。

示例:动画策略

// 策略接口
class AnimationStrategy {
  animate(element) {
    throw new Error('This method should be overridden by subclasses')
  }
}

// 具体策略类:淡入动画
class FadeInAnimation extends AnimationStrategy {
  animate(element) {
    element.style.transition = 'opacity 1s'
    element.style.opacity = 1
  }
}

// 具体策略类:滑动动画
class SlideInAnimation extends AnimationStrategy {
  animate(element) {
    element.style.transition = 'transform 1s'
    element.style.transform = 'translateX(0)'
  }
}

// 上下文类
class Animator {
  constructor(strategy) {
    this.strategy = strategy
  }

  setStrategy(strategy) {
    this.strategy = strategy
  }

  animate(element) {
    this.strategy.animate(element)
  }
}

// 使用示例
const element = document.getElementById('myElement')
const animator = new Animator(new FadeInAnimation())

animator.animate(element) // 应用淡入动画

animator.setStrategy(new SlideInAnimation())
animator.animate(element) // 应用滑动动画

请求处理

针对不同的数据请求,采取不同的处理策略,例如缓存策略、重试策略等,可以提高请求处理的效率和灵活性。

示例:请求处理策略

// 策略接口
class RequestStrategy {
  handleRequest(url) {
    throw new Error('This method should be overridden by subclasses')
  }
}

// 具体策略类:缓存策略
class CacheRequest extends RequestStrategy {
  constructor(cache) {
    super()
    this.cache = cache
  }

  handleRequest(url) {
    if (this.cache[url]) {
      return Promise.resolve(this.cache[url])
    } else {
      return fetch(url)
        .then((response) => response.json())
        .then((data) => {
          this.cache[url] = data
          return data
        })
    }
  }
}

// 具体策略类:重试策略
class RetryRequest extends RequestStrategy {
  handleRequest(url, retries = 3) {
    return fetch(url)
      .then((response) => response.json())
      .catch((error) => {
        if (retries > 0) {
          return this.handleRequest(url, retries - 1)
        } else {
          throw error
        }
      })
  }
}

// 上下文类
class RequestHandler {
  constructor(strategy) {
    this.strategy = strategy
  }

  setStrategy(strategy) {
    this.strategy = strategy
  }

  handleRequest(url) {
    return this.strategy.handleRequest(url)
  }
}

// 使用示例
const cache = {}
const requestHandler = new RequestHandler(new CacheRequest(cache))

requestHandler.handleRequest('https://api.example.com/data').then((data) => console.log(data))

requestHandler.setStrategy(new RetryRequest())
requestHandler.handleRequest('https://api.example.com/data').then((data) => console.log(data))

组件渲染

在不同的设备或浏览器环境下选择不同的渲染策略,例如回退到更兼容的组件方案,可以提高组件的兼容性和用户体验。

示例:组件渲染策略

// 策略接口
class RenderStrategy {
  render(element) {
    throw new Error('This method should be overridden by subclasses')
  }
}

// 具体策略类:现代渲染
class ModernRender extends RenderStrategy {
  render(element) {
    element.innerHTML = "<div class='modern'>Modern Rendering</div>"
  }
}

// 具体策略类:兼容渲染
class LegacyRender extends RenderStrategy {
  render(element) {
    element.innerHTML = "<div class='legacy'>Legacy Rendering</div>"
  }
}

// 上下文类
class Renderer {
  constructor(strategy) {
    this.strategy = strategy
  }

  setStrategy(strategy) {
    this.strategy = strategy
  }

  render(element) {
    this.strategy.render(element)
  }
}

// 使用示例
const element = document.getElementById('myElement')
const renderer = new Renderer(new ModernRender())

renderer.render(element) // 应用现代渲染

renderer.setStrategy(new LegacyRender())
renderer.render(element) // 应用兼容渲染

功能模块插拔

根据不同的业务需求动态切换功能模块或算法,如换肤功能、多种排序算法等,可以使得系统更加灵活和可扩展。

示例:换肤功能

// 策略接口
class ThemeStrategy {
  applyTheme(element) {
    throw new Error('This method should be overridden by subclasses')
  }
}

// 具体策略类:黑暗主题
class DarkTheme extends ThemeStrategy {
  applyTheme(element) {
    element.style.backgroundColor = '#333'
    element.style.color = '#fff'
  }
}

// 具体策略类:明亮主题
class LightTheme extends ThemeStrategy {
  applyTheme(element) {
    element.style.backgroundColor = '#fff'
    element.style.color = '#000'
  }
}

// 上下文类
class Themer {
  constructor(strategy) {
    this.strategy = strategy
  }

  setStrategy(strategy) {
    this.strategy = strategy
  }

  applyTheme(element) {
    this.strategy.applyTheme(element)
  }
}

// 使用示例
const element = document.getElementById('myElement')
const themer = new Themer(new DarkTheme())

themer.applyTheme(element) // 应用黑暗主题

themer.setStrategy(new LightTheme())
themer.applyTheme(element) // 应用明亮主题

典型案例

Validator.js

Validator.js 是一个字符串验证库,通过策略模式实现了多种验证算法,如邮箱验证、手机号验证、URL验证等。

const validator = require('validator')

console.log(validator.isEmail('[email protected]')) // true
console.log(validator.isMobilePhone('1234567890', 'en-US')) // true
console.log(validator.isURL('https://example.com')) // true

Day.js

Day.js 是一个轻量级的日期处理库,通过策略模式实现了各种日期格式化和操作算法。

const dayjs = require('dayjs')

console.log(dayjs().format('YYYY-MM-DD')) // 输出当前日期
console.log(dayjs('2022-01-01').add(1, 'month').format('YYYY-MM-DD')) // 2022-02-01
console.log(dayjs('2022-01-01').isBefore('2022-12-31')) // true

优缺点

优点

  1. 策略集的独立性:
    • 每个策略都是独立的类,允许我们将算法的各个变体封装起来,从而使它们可以互相替换。
    • 增加新的策略类非常方便,只需要实现策略接口即可,无需修改已经存在的策略类或上下文类。
  2. 遵循开闭原则:
    • 策略模式使得添加新的算法变得更加容易,通过增加新的策略类可以实现新的算法,而无需更改现有的代码。
    • 通过此模式,可以在运行时选择不同的算法,从而提供了可扩展性和灵活性。
  3. 减少条件语句:
    • 使用策略模式可以避免在客户端代码中使用大量的条件语句来选择不同的算法。通过策略接口,客户端只需了解抽象策略,而不需要关心具体的策略实现。
    • 这种设计使得代码更加清晰易读,维护成本降低。
  4. 更好的单一职责原则:
    • 策略模式将不同的行为封装在独立的策略类中,从而使每个类都有各自明确的职责。这种分离使得每个策略类只关注一种特定的算法,更符合单一职责原则。

缺点

  1. 增加类的数量:
    • 策略模式的一个显著缺点是会增加类的数量。每个策略都需要一个独立的类,如果有很多策略,就会导致类的数量剧增,增加系统的复杂度。
    • 过多的类也会增加代码的维护难度,需要合理设计和管理类层次关系。
  2. 客户端必须认识到所有策略:
    • 客户端代码需要了解所有的策略类以及它们之间的差异,并且需要知道何时使用何种策略。这种需求会使客户端代码对策略的了解程度增加,从而增加了客户端代码的复杂度。
  3. 策略类间可能代码重复:
    • 不同策略类之间可能会出现相同的代码,因为它们都需要实现策略接口并提供逻辑相似的功能。这种情况下需要注意代码的复用和优化,避免重复代码。

总结

策略模式通过将各种算法封装在独立的策略类中,使得算法可以互相替换,增加了系统的灵活性和可维护性。策略模式在前端开发中的应用场景广泛,包括表单验证、动画策略、请求处理、组件渲染和功能模块插拔等。通过策略模式,可以在运行时选择合适的算法或行为,提高代码的灵活性和可维护性。然而,策略模式也带来了类数量增加和客户端复杂度增加的问题,使用时需要权衡其优缺点,合理设计系统结构。