📌

ストラテジパターン

2024/11/07に公開

よくこんなコードを書いてしまう

if文やswitch-case文の使用

例:注文の配送方法(標準配送、速達配送、国際配送)によって料金が変わる

type ShippingType = "standard" | "express" | "international";

class Order
  weight: number;
  shippingType: ShippingType;

  constructor() {
    this.weight = number;
    this.shippingType = shippingType;
  }

  getShippingCost(): number {
    switch(this.shippingType) {
      case "standard"
        return 500; // 一律500円
      case "express"
        return 1000; // 一律1000円
      case "international"
        return this.weight * 200; // 重量に応じて200円/kg
      default
        retrun 0;
    }
  }
end

const standardOrder = new Order(10, "standard");
console.log(standardOrder.getShippingCost()); // 500

const expressOrder = new Order(10, "express");
console.log(expressOrder.getShippingCost()); // 1000

const internationalOrder = new Order(10, "international");
console.log(internationalOrder.getShippingCost()); // 2000 (10kg * 200)

処理の切り替え対象(配送方法)は今後もっと増えていく可能性がある。
そうなると、switch-case文は爆発的に増えていってしまう。

ストラテジーパターン

Strategy (ストラテジー、 戦略) は、 振る舞いに関するデザインパターンの一つで、 アルゴリズムのファミリーを定義し、 それぞれのアルゴリズムを別個のクラスとし、 それらのオブジェクトを交換可能にします。

参考文献:https://refactoring.guru/ja/design-patterns/strategy

ストラテジーパターンの要素

  1. Strategy(戦略): アルゴリズムの共通インターフェースを定義。
  2. ConcreteStrategy(具体的な戦略): Strategyインターフェースを実装する具体的なアルゴリズム。
  3. Context(コンテキスト): Strategyを使用するクラスで、具体的な戦略を選択し、実行。

実際に試してみる

  1. Strategy
// ShippingStrategy.ts
export interface ShippingStrategy {
  calculateShippingCost(weight: number): number;
}
  1. ConcreteStrategy
// StandardShipping.ts
import { ShippingStrategy } from "./ShippingStrategy";

export class StandardShipping implements ShippingStrategy {
  calculateShippingCost(weight: number): number {
    return 500; // 一律500円
  }
}

// ExpressShipping.ts
import { ShippingStrategy } from "./ShippingStrategy";

export class ExpressShipping implements ShippingStrategy {
  calculateShippingCost(weight: number): number {
    return 1000; // 一律1000円
  }
}

// InternationalShipping.ts
import { ShippingStrategy } from "./ShippingStrategy";

export class InternationalShipping implements ShippingStrategy {
  calculateShippingCost(weight: number): number {
    return weight * 200; // 重量に応じて200円/kg
  }
}

implementsとは
クラスがプロパティやメソッドをインターフェースで定義された形でもつように強制している
https://typescriptbook.jp/reference/object-oriented/interface/implementing-interfaces

  1. Context
// Order.ts
import { ShippingStrategy } from "./ShippingStrategy";

export class Order {
  weight: number;
  private shippingStrategy: ShippingStrategy;

  constructor(weight: number, shippingStrategy: ShippingStrategy) {
    this.weight = weight;
    this.shippingStrategy = shippingStrategy;
  }

  // 後から配送方法を変更する際
  setShippingStrategy(strategy: ShippingStrategy): void {
    this.shippingStrategy = strategy;
  }

  getShippingCost(): number {
    return this.shippingStrategy.calculateShippingCost(this.weight);
  }
}

使用例

const standardOrder = new Order(10, new StandardShipping());
console.log(standardOrder.getShippingCost()); // 500

const expressOrder = new Order(10, new ExpressShipping());
console.log(expressOrder.getShippingCost()); // 1000

const internationalOrder = new Order(10, new InternationalShipping());
console.log(internationalOrder.getShippingCost()); // 2000 (10kg * 200)

Railsでも試してみる

まず、悪い?例

class Order < ApplicationRecord
  enum shipping_type: { standard: 0, express: 1, international: 2 }

  def shipping_cost
    case shipping_type
    when "standard"
      500 # 一律500円
    when "express"
      1000 # 一律1000円
    when "international"
      weight * 200 # 重量に応じて200円/kg
    else
      0
    end
  end
end

standard_order = Order.new(weight: 10, shipping_type: "standard")
puts standard_order.shipping_cost #=> 500

express_order = Order.new(weight: 10, shipping_type: "express")
puts express_order.shipping_cost #=> 1000

international_order = Order.new(weight: 10, shipping_type "international")
puts international_order.shipping_cost #=> 2000 (10kg * 200)

実際にストラテジーパターンで変更していくと、、、

  1. Strategy
# app/strategies/shipping_strategy.rb
class ShippingStrategy
  def calculate_shipping_cost(order)
    raise NotImplementedError, "You must implement the calculate_shipping_cost method"
  end
end
  1. ConcreteStrategy
# app/strategies/standard_shipping.rb
class StandardShipping < ShippingStrategy
  def calculate_shipping_cost(order)
    500 # 一律500円
  end
end

# app/strategies/express_shipping.rb
class ExpressShipping < ShippingStrategy
  def calculate_shipping_cost(order)
    1000 # 一律1000円
  end
end

# app/strategies/international_shipping.rb
class InternationalShipping < ShippingStrategy
  def calculate_shipping_cost(order)
    order.weight * 200 # 重量に応じて200円/kg
  end
end

(interfaceやimplementsは無いため、以下のようにmoduleで対応)

  1. Context
# app/models/order.rb
class Order < ApplicationRecord
  def initialize(attributes = {})
    super
    @shipping_strategy = attributes[:shipping_type]
  end

  def set_shipping_strategy(strategy)
    @shipping_strategy = strategy
  end

  def shipping_cost
    @shipping_strategy.calculate_shipping_cost(self)
  end
end

使用例

standard_order = Order.new(weight: 10, shipping_type: StandardShipping.new)
puts standard_order.shipping_cost #=> 500

express_order = Order.new(weight: 10, shipping_type: ExpressShipping.new)
puts express_order.shipping_cost #=> 1000

international_order = Order.new(weight: 10, shipping_type InternationalShipping.new)
puts international_order.shipping_cost #=> 2000 (10kg * 200)

ただし、これだと配送方法がdbに保存されていないので、あとから参照ができない。
その解決策としては、polymorphicを使うのはどうだろうか、、、

  1. Strategy
# app/strategies/shipping_strategy.rb
module ShippingStrategy
  def calculate_shipping_cost(order)
    raise NotImplementedError, "You must implement the calculate_shipping_cost method"
  end
end
  1. ConcreteStrategy
# app/models/shipping_strategies/standard_shipping.rb
class StandardShipping < ApplicationRecord
  include ShippingStrategy
  def calculate_shipping_cost(order)
    500 # 一律500円
  end
end

# app/models/shipping_strategies/express_shipping.rb
class ExpressShipping < ApplicationRecord
  include ShippingStrategy
  def calculate_shipping_cost(order)
    1000 # 一律1000円
  end
end

# app/models/shipping_strategies/international_shipping.rb
class InternationalShipping < ApplicationRecord
  include ShippingStrategy
  def calculate_shipping_cost(order)
    order.weight * 200 # 重量に応じて200円/kg
  end
end
  1. Context
# app/models/order.rb
class Order < ApplicationRecord
  belongs_to :shipping_strategy, polymorphic: true

  def shipping_cost
    shipping_strategy.calculate_shipping_cost(self)
  end
end

後は、今回のストラテジパターンではないかもだが、、、

# app/models/order.rb
class Order < ApplicationRecord
  enum shipping_type: { standard: 0, express: 1, international: 2 }
  attribute :weight, :integer

  # 各配送方法ごとの送料計算ロジックを定義
  SHIPPING_COST_CALCULATORS = {
    standard: ->(weight) { 500 },                 # 一律500円
    express: ->(weight) { 1000 },                 # 一律1000円
    international: ->(weight) { weight * 200 }    # 重量に応じて200円/kg
  }.freeze

  def get_shipping_cost
    calculator = SHIPPING_COST_CALCULATORS[shipping_type.to_sym]
    calculator ? calculator.call(weight) : 0
  end
end

参考文献

https://amzn.asia/d/06fywJv8

Discussion