📌
ストラテジパターン
よくこんなコードを書いてしまう
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
ストラテジーパターンの要素
- Strategy(戦略): アルゴリズムの共通インターフェースを定義。
- ConcreteStrategy(具体的な戦略): Strategyインターフェースを実装する具体的なアルゴリズム。
- Context(コンテキスト): Strategyを使用するクラスで、具体的な戦略を選択し、実行。
実際に試してみる
- Strategy
// ShippingStrategy.ts
export interface ShippingStrategy {
calculateShippingCost(weight: number): number;
}
- 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とは
クラスがプロパティやメソッドをインターフェースで定義された形でもつように強制している
- 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)
実際にストラテジーパターンで変更していくと、、、
- 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
- 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で対応)
- 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を使うのはどうだろうか、、、
- 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
- 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
- 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
参考文献
Discussion