はじめに
GoFのStrategyパターンを勉強したので、アウトプットのためにまとめてみました。
今回は、豚もも肉(脂身付き)の、各調理方法によるカロリーの違いを題材にします。
思ったより変化があるみたいで驚きました!
※本記事で使っている具体的なカロリー値は、以下のサイトの情報を元にした概算です。また、計算の都合上、比率の数値は丸めて使用しているので誤差が出ます。
https://dietplus.jp/public/article/news/20211104-490474
Strategyパターンとは
Strategyパターンは、処理(アルゴリズム)を個別のクラスに分け、実行時にその中から適切なものを選んで使うことができる設計パターンです。これにより、処理の切り替えや拡張がしやすくなります。
例えば今回の例では、「豚肉を調理する」という共通の処理に対して、「ゆでる」「焼く」「煮込む」「から揚げ」といった個別の調理方法(アルゴリズム)をそれぞれ独立したクラスに分けています。状況に応じて使いたい調理法のクラスを選ぶことで、同じ操作(調理)でも異なるふるまいを実現できます。これにより、新しい調理法を追加したい場合でも、既存のコードをほとんど変更せずに機能を拡張できます。
それぞれのクラスの役割
Strategy
共通の処理を定義するインターフェースです。
具体的な処理内容は、Concrete Strategyに書いていきます。
Concrete Strategy
Strategyインターフェースで定義された処理を、実際に実装するクラスです。
処理のバリエーションごとにクラスを分けて定義でき、新しい処理を追加したいときも、既存のコードを変更せずにクラスを追加するだけで済みます。
Context
Strategyを使って実際に処理を実行するクラスです。
具体的な処理(Concrete Strategy)については知らずに、Strategyインターフェースに定義されたメソッドだけを呼び出すため、後からConcrete Strategyで処理の追加や変更があっても、Contextのコードはほとんど変える必要がありません。
クライアントコード
豚もも肉の基準となるカロリーを入力したら、各調理方法による増減後のカロリーが出力されるようになっています。
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("豚もも肉(脂身付き)のカロリーを入力してください: ");
int baseCalorie = scanner.nextInt();
if (baseCalorie < 0) {
System.out.println("負の値は許容しません。");
return;
}
PorkCooker porkCooker = new PorkCooker();
porkCooker.executeCooking(new Boiling(), baseCalorie);
porkCooker.executeCooking(new Grilling(), baseCalorie);
porkCooker.executeCooking(new Stewing(), baseCalorie);
porkCooker.executeCooking(new DeepFrying(), baseCalorie);
}
}
// baseCalorie = 183 の時の出力例
// ゆでる: 134kcal
// 焼く: 146kcal
// 煮込む: 154kcal
// から揚げ: 201kcal
Strategy(調理方法の共通仕様)
ここでは「豚肉を調理する」「調理方法の名前を取得する」という共通処理の定義のみをしています。
※ getMethodName()
はUI表示のための補助的なメソッドで、Strategyパターンの本質ではありません。今回は簡潔さを優先してまとめているけれど、メソッドごとにインターフェースを分けて責務を分離するのが望ましいのかもしれない、、、
interface PorkCookingMethod {
double cook(int baseCalorie);
String getMethodName();
}
ConcreteStrategy(具体的な調理方法の定義)
ここでStrategyを具体的に実装していきます。
調理方法を増やしたいときは、基本的にはここを増やせばOKです。
class Boiling implements PorkCookingMethod {
public double cook(int baseCalorie) {
return baseCalorie * 0.732;
}
public String getMethodName() {
return "ゆでる";
}
}
class Grilling implements PorkCookingMethod {
public double cook(int baseCalorie) {
return baseCalorie * 0.798;
}
public String getMethodName() {
return "焼く";
}
}
class Stewing implements PorkCookingMethod {
public double cook(int baseCalorie) {
return baseCalorie * 0.841;
}
public String getMethodName() {
return "煮込む";
}
}
class DeepFrying implements PorkCookingMethod {
public double cook(int baseCalorie) {
return baseCalorie * 1.098;
}
public String getMethodName() {
return "から揚げ";
}
}
Context(調理をする人)
executeCooking
メソッドで Strategyの実装を受け取り、それに従って処理を実行します。
このクラスは Strategy インターフェースの存在だけを知っていればよく、具体的な実装(ConcreteStrategy)には依存しません。
そのため、新しい調理方法を追加しても、このクラスを修正する必要はありません。拡張に強く、柔軟な設計ができています。
class PorkCooker {
public void executeCooking(PorkCookingMethod method, int baseCalorie) {
double cookedCalorie = method.cook(baseCalorie);
String methodName = method.getMethodName();
System.out.printf("%s: %.0fkcal\n", methodName, cookedCalorie);
}
}
まとめ
今回の「豚肉の調理方法」という例では、処理内容(ゆでる/焼く/煮込むなど)をそれぞれクラスとして定義し、調理者(Context)がそれを使い分ける形で実装しました。
このように、処理のバリエーションがあり、あとから追加・変更が発生しそうな場面では、Strategyパターンは非常に効果的です。
ただ、やはりクラス数が増えてしまうので、小規模なプロジェクトでは控えた方が良い場合もあるかもしれません。
今後もし似たような構造を設計することがあれば、「処理の差し替えが必要か?」という観点からStrategyパターンの適用を検討してみると良いでしょう。
参考サイト