はじめに
新卒1年目エンジニアの私が増補改訂版 Java言語で学ぶデザインパターン入門を読んで「これは使えそう。勉強になった。復習したい。」と思ったデザインパターンを5つ紹介します。
結論から言うと、以下の5つのデザインパターンです。
- Template Methodパターン
- Prototypeパターン
- Abstract Factoryパターン
- Decoratorパターン
- Mediatorパターン
では、それぞれ紹介していきます。
Template Methodパターン
Template Methodパターンは、スーパークラスで処理の枠組みを定め、サブクラスでその固有の処理を定める方式です。
基本的な処理の流れが同じで、具体的な処理が違ったり、一部の処理のみ違ったりすることはよくあることだと思うので、知っておくと便利そうです。
サンプルプログラム
自宅トレーニーとジムトレーニーがトレーニングするときの手順を出力するプログラムです。
基本的な手順の流れは似ているためスーパークラスで処理のテンプレートを準備し、具体的な行動の内容はそれぞれ異なるためサブクラスで実装しています。
// 処理のテンプレートと具体的な処理を実装するメソッドの定義
public abstract class Trainee {
public abstract void prepare();
public abstract void trainPectoralMuscle();
public abstract void finish();
private void drinkProtein() {
System.out.println("プロテインを飲む");
}
public final void train() {
prepare(); // トレーニングの準備
for (int i = 0; i < 3; i++) { // トレーニングを3セット
trainPectoralMuscle();
}
drinkProtein(); // プロテインを飲む
finish(); // 終了
}
}
// Traineeを継承し、具体的処理の実装をする
public class HomeTrainee extends Trainee {
public void prepare() {
System.out.println("マットレスを引く");
}
public void trainPectoralMuscle() {
System.out.println("腕立て伏せを30回する");
}
public void finish() {
System.out.println("マットレスを片付ける");
}
}
// Traineeを継承し、具体的処理の実装をする
public class GymTrainee extends Trainee {
public void prepare() {
System.out.println("ジムに行く");
}
public void trainPectoralMuscle() {
System.out.println("ベンチプレスを10回する");
}
public void finish() {
System.out.println("ジムから帰る");
}
}
public class Main {
public static void main(String[] args){
HomeTrainee homeTrainee = new HomeTrainee();
GymTrainee gymTrainee = new GymTrainee();
System.out.println("**自宅トレー二ー**");
homeTrainee.train();
System.out.print("\n");
System.out.println("**ジムトレー二ー**");
gymTrainee.train();
}
}
実行結果
**自宅トレー二ー**
マットレスを引く
腕立て伏せを30回する
腕立て伏せを30回する
腕立て伏せを30回する
プロテインを飲む
マットレスを片付ける
**ジムトレー二ー**
ジムに行く
ベンチプレスを10回する
ベンチプレスを10回する
ベンチプレスを10回する
プロテインを飲む
ジムから帰る
このように基本的な処理の構成は同じだけど、具体的な処理が違ったり、一部の処理が違うクラスが複数ある場合に使用すると便利そうです。
Prototypeパターン
Prototypeパターンはインスタンスをコピーして、新しいインスタンスを生成する方式です。
本書によると、このパターンを使う場面は以下のようなときだそうです。
- 種類が多すぎてクラスにまとめられない場合
- クラスからのインスタンスの生成が難しい場合
- フレームワークと生成するインスタンスを分けたいと場合
あまり使う場面はなさそうな気がしますが、インスタンスを登録しておき、再利用するということが勉強になりました。
サンプルプログラム
文字列を修飾するインスタンスを作成、登録しておき、使用するときに再利用するようなプログラムです。
public interface Decorator extends Cloneable {
// 文字列を修飾して出力
public abstract void decorate(String value);
// インスタンスの複製
public abstract Decorator createClone();
}
public class Manager {
private HashMap showcase = new HashMap();
// インスタンスの登録
public void register(String name, Decorator decorator) {
showcase.put(name, decorator);
}
// インスタンスの複製
public Decorator create(String name) {
Decorator decorator = (Decorator)showcase.get(name);
return decorator.createClone();
}
}
public class HeadDecorator implements Decorator {
private String head;
public HeadDecorator(String head) {
this.head = head;
}
public void decorate(String value) {
System.out.println(head + value);
}
public Decorator createClone() {
Decorator decorator = null;
try {
decorator = (Decorator)clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return decorator;
}
}
public class BothEndsDecorator implements Decorator {
private String head;
private String tail;
public BothEndsDecorator(String head, String tail) {
this.head = head;
this.tail = tail;
}
// 文字列を修飾して出力
public void decorate(String value) {
System.out.println(head + value + tail);
}
// インスタンスの複製
public Decorator createClone() {
Decorator decorator = null;
try {
decorator = (Decorator)clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return decorator;
}
}
public class Main {
public static void main(String[] args){
Manager manager = new Manager();
HeadDecorator hd1 = new HeadDecorator("# ");
HeadDecorator hd2 = new HeadDecorator("- ");
BothEndsDecorator bed1 = new BothEndsDecorator("**", "**");
manager.register("h1", hd1);
manager.register("list", hd2);
manager.register("bold", bed1);
Decorator h1 = manager.create("h1");
h1.decorate("今日の筋トレ");
Decorator list = manager.create("list");
list.decorate("腹筋30回 × 3セット");
Decorator bold = manager.create("bold");
bold.decorate("POWER!");
}
}
実行結果
# 今日の筋トレ
- 腹筋30回 × 3セット
**POWER!**
Prototypeパターンはクラス利用者が具象オブジェクトの情報を保持しておく必要がないというメリットがあるので、今回のサンプルプログラムではインスタンスを3つしか生成していませんが、種類が多すぎてクラスにまとめられない場合などに使えそうです。
Abstract Factoryパターン
Abstract FactoryパターンはFactory of Factoriesと呼ばれており、クライアントが抽象クラスを使って、具体的な製品を知ることなく製品を作成することができます。
個人的にこのデザインパターンは他のパターンと比べて複雑であり、紹介している記事によって微妙に構成が異なっていて理解に時間がかかりました。
紹介している記事によって構成が若干異なっていましたが、クライアントが具体的なFactoryクラスの情報を持たずに製品を作れるという点はどの説明でも一致しており、ここがAbstract Factoryパターンの大事な点となります。
クラス図

上の図を用いて説明します。
AbstractFactoryはAbstractProductのインスタンスを生成するためのインタフェースです。
AbstractProductは抽象的な部品のインタフェースです。
ConcreateFactoryはAbstractFactoryを継承した具体的なクラス、ConcreateProductはAbstractProductを継承した具体的なクラスです。
サンプルプログラム
ジムトレーニーと自宅トレーニーのトレーニングメニューを作成するプログラムです。
以下のクラスに注目してみてください。
- トレーニングメニューの抽象クラス、具象クラス
- トレーニングメニューを作成する抽象クラス、具象クラス
main関数は抽象的なFactoryクラスは知っていますが、具体的なFactoryクラスは知らない状態でトレーニングメニューを作成します。
// 抽象的なトレーニングメニューのFactoryクラス
public abstract class TrainingMenuFactory {
abstract TrainingMenu createTrainingMenu();
// 具体的なトレーニングメニューのFactoryクラスを取得
public static TrainingMenuFactory getFactory(String place) {
if (place.equals("ジム")) {
return new GymTrainingMenuFactory();
} else if (place.equals("自宅")) {
return new HomeTrainingMenuFactory();
} else {
return null;
}
}
}
// 具体的なトレーニングメニューのFactoryクラス (自宅用トレーニングメニュー)
public class HomeTrainingMenuFactory extends TrainingMenuFactory {
// 自宅用トレーニングメニューを作成
TrainingMenu createTrainingMenu() {
return new HomeTrainingMenu();
}
}
// 具体的なトレーニングメニューのFactoryクラス (ジム用トレーニングメニュー)
public class GymTrainingMenuFactory extends TrainingMenuFactory {
// ジム用トレーニングメニューを作成
TrainingMenu createTrainingMenu() {
return new GymTrainingMenu();
}
}
import java.util.ArrayList;
// 抽象的なトレーニングメニュークラス
public abstract class TrainingMenu {
public abstract String place();
public abstract ChestMenu chestMenu();
public abstract LegMenu legMenu();
protected ArrayList<MenuItem> menus = new ArrayList();
// 胸のトレーニングを追加
public void addChestMenu() {
menus.add(chestMenu());
}
// 脚のトレーニングを追加
public void addLegMenu() {
menus.add(legMenu());
}
// トレーニングメニューを出力
public final void displayMenu() {
System.out.println("# " + place() + "でのトレーニングメニュー");
menus.forEach(menu -> menu.display());
}
}
// 具体的なトレーニングメニュークラス (自宅用トレーニングメニュー)
public class HomeTrainingMenu extends TrainingMenu {
public String place() {
return "自宅";
}
public ChestMenu chestMenu() {
return new HomeChestMenu();
}
public LegMenu legMenu() {
return new HomeLegMenu();
}
}
// 具体的なトレーニングメニュークラス (ジム用トレーニングメニュー)
public class GymTrainingMenu extends TrainingMenu {
public String place() {
return "ジム";
}
public ChestMenu chestMenu() {
return new GymChestMenu();
}
public LegMenu legMenu() {
return new GymLegMenu();
}
}
// 抽象的なトレーニングメニューの部品クラス
public interface MenuItem {
public abstract void display();
}
// 抽象的な胸トレーニングメニューの部品クラス
public abstract class ChestMenu implements MenuItem {}
// 抽象的な脚トレーニングメニューの部品クラス
public abstract class LegMenu implements MenuItem {}
// 具体的な胸トレーニングメニューの部品クラス (自宅用胸トレーニングメニュー)
public class HomeChestMenu extends ChestMenu {
public void display() {
System.out.println("- 腕立て伏せ");
}
}
// 具体的な胸トレーニングメニューの部品クラス (ジム用胸トレーニングメニュー)
public class GymChestMenu extends ChestMenu {
public void display() {
System.out.println("- ベンチプレス");
}
}
// 具体的な脚トレーニングメニューの部品クラス (自宅用胸トレーニングメニュー)
public class HomeLegMenu extends LegMenu {
public void display() {
System.out.println("- スクワット");
}
}
// 具体的な脚トレーニングメニューの部品クラス (ジム用胸トレーニングメニュー)
public class GymLegMenu extends LegMenu {
public void display() {
System.out.println("- バーベルスクワット");
}
}
public class Main {
public static void main(String[] args){
// 1. トレーニングメニューFactoryを取得
// 2. トレーニングメニューを生成
// 3. トレーニングメニューにトレーニングを追加
// 4. トレーニングメニューを出力
TrainingMenuFactory trainingMenuFactory1 = TrainingMenuFactory.getFactory("ジム");
TrainingMenu trainingMenu1 = trainingMenuFactory1.createTrainingMenu();
trainingMenu1.addChestMenu();
trainingMenu1.addLegMenu();
trainingMenu1.displayMenu();
System.out.println();
TrainingMenuFactory trainingMenuFactory2 = TrainingMenuFactory.getFactory("自宅");
TrainingMenu trainingMenu2 = trainingMenuFactory2.createTrainingMenu();
trainingMenu2.addChestMenu();
trainingMenu2.displayMenu();
}
}
実行結果
# ジムでのトレーニングメニュー
- ベンチプレス
- バーベルスクワット
# 自宅でのトレーニングメニュー
- 腕立て伏せ
Abstract Factoryパターンを使用すると、Client(サンプルプログラムではMain関数)は具体的なFactoryクラスの情報を持たないため、Clientに影響を出さずに新たな具体的なFactoryクラスを追加しやすくなります。
ですがデメリットもあり、新たな部品クラスを作成するとなると、全ての具体的なFactoryクラスの改修が必要になってしまいます。
Decoratorパターン
Decoratorパターンはオブジェクトに修飾を行うパターンです。このパターンを利用することにより元のクラスに変更を加えることなく機能の拡張が行なえます。
サンプルプログラム
Decoratorパターンを使用して、トレーニング用のダンベルを作成し表示させるプログラムです。
まずはダンベルのバーの部分を生成し、そのバーにプレートを装着(修飾)して表示します。
public abstract class Dumbbell {
// ダンベルの幅
public abstract int getWidth();
// ダンベルの高さ
public abstract int getHeight();
public abstract String getString(int height);
// ■ でダンベルの見た目を表現する
public final String getChar() {
return "■ ";
}
public final String getEmptyString(int width) {
String string = "";
for (int i = 0; i < width; i++) {
string += " ";
}
return string;
}
// ダンベルを表示
public final void display() {
for (int i = 0; i < getHeight(); i++) {
System.out.println(getString(i - getHeight() / 2));
}
}
}
// ダンベルのバーを表すクラス
public class Bar extends Dumbbell {
private int width;
public Bar(int width) {
this.width = width;
}
public int getWidth() {
return width;
}
public int getHeight() {
return 1;
}
public String getString(int height) {
if (height == 0) {
String string = "";
for (int i = 0; i < getWidth(); i++) {
string += getChar();
}
return string;
} else {
return getEmptyString(getWidth());
}
}
}
// ダンベルのプレートを表す抽象クラス
public abstract class Plate extends Dumbbell {
protected Dumbbell dumbbell;
protected Plate(Dumbbell dumbbell) {
this.dumbbell = dumbbell;
}
}
// ダンベルのプレートを表す具象クラス
public class LargePlate extends Plate {
protected LargePlate(Dumbbell dumbbell) {
super(dumbbell);
}
public int getWidth() {
return 1 + dumbbell.getWidth() + 1;
}
public int getHeight() {
return Math.max(dumbbell.getHeight(), 5);
}
public String getString(int height) {
if (-2 <= height && height <= 2) {
return getChar() + dumbbell.getString(height) + getChar();
} else {
return getEmptyString(1) + dumbbell.getString(height) + getEmptyString(1);
}
}
}
// ダンベルのプレートを表す具象クラス
public class SmallPlate extends Plate {
protected SmallPlate(Dumbbell dumbbell) {
super(dumbbell);
}
public int getWidth() {
return 1 + dumbbell.getWidth() + 1;
}
public int getHeight() {
return Math.max(dumbbell.getHeight(), 3);
}
public String getString(int height) {
if (-1 <= height && height <= 1) {
return getChar() + dumbbell.getString(height) + getChar();
} else {
return getEmptyString(1) + dumbbell.getString(height) + getEmptyString(1);
}
}
}
public class Main {
public static void main(String[] args) {
Dumbbell dumbbell1 =
new SmallPlate(
new Bar(3)
);
System.out.println("小さいダンベル");
dumbbell1.display();
System.out.println("");
Dumbbell dumbbell2 =
new SmallPlate(
new LargePlate(
new LargePlate(
new Bar(4)
)
)
);
System.out.println("大きいダンベル");
dumbbell2.display();
}
}
実行結果
小さいダンベル
■ ■
■ ■ ■ ■ ■
■ ■
大きいダンベル
■ ■ ■ ■
■ ■ ■ ■ ■ ■
■ ■ ■ ■ ■ ■ ■ ■ ■ ■
■ ■ ■ ■ ■ ■
■ ■ ■ ■
Decoratorパターンを使って中身を変更せずオブジェクトに対して修飾を行うことができました。また、修飾を行うクラス(サンプルプログラムのPlateクラス)の作りが単純であっても修飾の数や順序、種類によって多様な機能追加でできます。
しかし、機能追加をするためにクラスで包んでいくと、元となるオブジェクト(サンプルプログラムのBarオブジェクト)を参照するために一つ内側を参照する処理を繰り返すことになるというデメリットもあります。
Mediatorパターン
mediatorは仲介者という意味で、Mediatorパターンは複雑であったり、数が多いオブジェクト間の関係を仲介者を通して処理を行うことで、それぞれのオブジェクト間の結合度を下げるパターンです。
言葉では分かりにくいと思うので、サンプルプログラムで説明していきます。
サンプルプログラム
とあるスポーツジムでは、トレーニングをする際、安全のために必ずサポートしてくれる人が必要です。
初心者トレーニーは、上級者にサポートしてもらい、上級者トレーニーは初心者にサポートしてもらいます。
そのため、このジムではトレーニングを始める前にジムにいるトレーニーの中からサポーター役を探す必要があります。
このとき、トレーニングをする人が一人ひとりに「サポートしてくれますか?」と聞いて回るのではなく、サポーターを探してくれる仲介者に「サポーターを探してください」とお願いするのです。
// 仲介者のインターフェース
public interface Mediator {
// ジム内のトレーニーを追加
public abstract void addTrainee(Trainee trainee);
// サポーターを探す
public Optional<Trainee> findSupporter(Class<? extends Trainee> traineeClass);
}
// 仲介者のサブクラス
public class SupporterFinder implements Mediator {
// ジム内のトレーニーを追加
private ArrayList<Trainee> trainees = new ArrayList();
public void addTrainee(Trainee trainee) {
trainees.add(trainee);
}
// サポーターを探す
public Optional<Trainee> findSupporter(Class<? extends Trainee> traineeClass) {
for (Trainee trainee : trainees) {
if (trainee.getClass() == traineeClass && trainee.getStatus() == Status.FLEE) {
return Optional.of(trainee);
}
}
return Optional.empty();
}
}
// トレーニーの状態を表すクラス
public enum Status {
FLEE("フリー"),
TRAINING("トレーニング中"),
SUPPORTING("サポート中");
String value;
Status(String value) {
this.value = value;
}
}
// トレーニーの抽象クラス
public abstract class Trainee {
public String name;
public Mediator mediator;
private Status status; // ['フリー', 'トレーニング中', 'サポート中']
protected Trainee(String name, Mediator mediator) {
this.name = name;
this.mediator = mediator;
this.status = Status.FLEE;
System.out.println(name + "さんがジムに入りました。 現在" + status.value + "です。");
mediator.addTrainee(this);
}
public Status getStatus() {
return this.status;
}
public void setStatus(Status status) {
System.out.println(name + "さんの状態が変更されました。 現在" + status.value + "です。");
this.status = status;
}
// トレーニングを開始
public abstract void startTraining();
}
import java.util.Optional;
// トレーニーのサブクラス
public class BeginnerTrainee extends Trainee {
protected BeginnerTrainee(String name, Mediator mediator) {
super(name, mediator);
}
// トレーニングを開始
public void startTraining() {
// 仲介者にサポーターを探してもらう
Optional<Trainee> optionalSupporter = mediator.findSupporter(SeniorTrainee.class);
if (optionalSupporter.isPresent()) {
Trainee supporter = optionalSupporter.get();
this.setStatus(Status.TRAINING);
supporter.setStatus(Status.SUPPORTING);
} else {
System.out.println(name + "さんのサポーターが見つかりませんでした。");
}
}
}
import java.util.Optional;
// トレーニーのサブクラス
public class SeniorTrainee extends Trainee {
protected SeniorTrainee(String name, Mediator mediator) {
super(name, mediator);
}
// トレーニングを開始
public void startTraining() {
// 仲介者にサポーターを探してもらう
Optional<Trainee> optionalSupporter = mediator.findSupporter(BeginnerTrainee.class);
if (optionalSupporter.isPresent()) {
Trainee supporter = optionalSupporter.get();
this.setStatus(Status.TRAINING);
supporter.setStatus(Status.SUPPORTING);
} else {
System.out.println(name + "さんのサポーターが見つかりませんでした。");
}
}
}
public class Main {
public static void main(String[] args) {
SupporterFinder supporterFinder = new SupporterFinder();
SeniorTrainee seniorTrainee1 = new SeniorTrainee("たかし", supporterFinder);
SeniorTrainee seniorTrainee2 = new SeniorTrainee("ひろかず", supporterFinder);
SeniorTrainee seniorTrainee3 = new SeniorTrainee("ゆうじ", supporterFinder);
SeniorTrainee seniorTrainee4 = new SeniorTrainee("だいすけ", supporterFinder);
BeginnerTrainee beginnerTrainee1 = new BeginnerTrainee("たろう", supporterFinder);
BeginnerTrainee beginnerTrainee2 = new BeginnerTrainee("かずや", supporterFinder);
System.out.println();
seniorTrainee1.startTraining();
beginnerTrainee2.startTraining();
seniorTrainee3.startTraining();
}
}
実行結果
たかしさんがジムに入りました。 現在フリーです。
ひろかずさんがジムに入りました。 現在フリーです。
ゆうじさんがジムに入りました。 現在フリーです。
だいすけさんがジムに入りました。 現在フリーです。
たろうさんがジムに入りました。 現在フリーです。
かずやさんがジムに入りました。 現在フリーです。
たかしさんの状態が変更されました。 現在トレーニング中です。
たろうさんの状態が変更されました。 現在サポート中です。
かずやさんの状態が変更されました。 現在トレーニング中です。
ひろかずさんの状態が変更されました。 現在サポート中です。
ゆうじさんのサポーターが見つかりませんでした。
このように仲介者を通して処理をすることによって、トレーニー同士がお互いの情報を持たずに済むためとても身軽になりました。
登場人物が多く、それぞれの人物の行動にお互いの情報が必要な場合、このデザインパターンが役に立ちそうです。
まとめ
今回は増補改訂版 Java言語で学ぶデザインパターン入門を読んで復習したいと思った5つのデザインパターンを紹介しました。
- Template Methodパターン
- Prototypeパターン
- Abstract Factoryパターン
- Decoratorパターン
- Mediatorパターン
最後までご覧いただきありがとうございました!🙇