オブジェクト指向プログラミングで開発を進めていると、オブジェクト単体の設計だけでなく、「複数のオブジェクトをどう協調させるか」という連携方法に悩まされる場面が多々あります。クラス同士が複雑に密結合してしまうと、一部分を修正しただけで思わぬところに影響が及び、デバッグ作業に追われることになりかねません。
そのような、オブジェクト間の関係性や処理の流れを整理し、協調動作をスムーズにするための設計手法。それがGoF(Gang of Four)デザインパターンの「振る舞いに関するパターン(Behavioral Patterns)」です。今回は、このカテゴリーに分類される11種類のパターンについて、使い所とJavaScriptによる簡潔な実装例を交えて整理していきます。
1. 振る舞いに関するパターンとは
振る舞いに関するパターンは、オブジェクトやクラス間の通信、および責任の分担に焦点を当てています。単にデータ構造を組み合わせるのではなく、アルゴリズムの制御フローを柔軟に変更したり、イベントを効率的に伝達したりするための工夫が凝らされているのが特徴です。全11パターンを順番に紹介します。
2. Template Method(テンプレート・メソッド)
使い所
全体の処理の大枠(アルゴリズムの骨組み)は共通しているものの、一部の具体的な手順だけを状況に応じて変更したい場合に使用します。
使い方とサンプルコード
親クラスで全体の処理フローを定義し、個々の具体的な処理ステップは抽象メソッド(またはデフォルト処理を持つメソッド)として定義して、子クラスでそれをオーバーライドさせます。
// 抽象クラス(骨組みを定義)
class ArticlePublisher {
publish() {
let content = this.writeBody();
content = this.formatMarkup(content);
this.output(content);
}
writeBody() {
throw new Error("このメソッドはオーバーライドする必要があります。");
}
formatMarkup(content) {
// デフォルトのフォーマット処理
return `${content}
`;
}
output(content) {
console.log(`[公開完了] ${content}`);
}
}
// 具体的な実装クラス
class TechArticlePublisher extends ArticlePublisher {
writeBody() {
return "Gitのコンフリクトを解消する方法";
}
formatMarkup(content) {
// 技術記事向けにマークアップを差し替える
return `${content}`;
}
}
const publisher = new TechArticlePublisher();
publisher.publish(); // [公開完了] Gitのコンフリクトを解消する方法
何が便利なのか
処理の全体フローを1カ所で一元管理できるため、流れそのもののバグを減らすことができます。サブクラス側は一部の変更したいメソッドのみを記述すればよいため、コードの重複を防ぎます。
3. Iterator(イテレーター)
使い所
リストやツリー、グラフなど、集約オブジェクトの内部構造(どのようにデータを持っているか)を利用側に意識させることなく、すべての要素に順番にアクセスさせたい場合に使用します。
使い方とサンプルコード
JavaScriptでは、言語標準の [Symbol.iterator] を実装することで、独自のクラスであっても for...of などの構文に対応させることができます。
// 本を表すクラス
class Book {
constructor(title) {
this.title = title;
}
}
// 本棚(集約体)を表すクラス
class BookShelf {
constructor() {
this.books = [];
}
addBook(book) {
this.books.push(book);
}
// イテレーターの定義
[Symbol.iterator]() {
let index = 0;
const books = this.books;
return {
next() {
return index < books.length
? { value: books[index++], done: false }
: { done: true };
}
};
}
}
const shelf = new BookShelf();
shelf.addBook(new Book("デザインパターン入門"));
shelf.addBook(new Book("Refactoring"));
for (const book of shelf) {
console.log(book.title);
}
何が便利なのか
データの格納形式が配列から別のデータ構造(例えばツリー構造など)に変わったとしても、ループ処理を行う呼び出し側のコードに影響を与えないという強みがあります。
4. Chain of Responsibility(責任の連鎖)
使い所
ひとつの要求に対して複数のオブジェクトが対処できる可能性があり、どのオブジェクトが処理を処理すべきかを実行時に決定したい場合に適しています。
使い方とサンプルコード
リクエストを受け取るハンドラクラスを数珠つなぎ(チェーン)にし、自分が処理できないリクエストであれば次のハンドラへ転送するように実装します。
class SupportHandler {
setNext(handler) {
this.nextHandler = handler;
return handler;
}
handle(request) {
if (this.nextHandler) {
return this.nextHandler.handle(request);
}
return "誰も対応できませんでした。";
}
}
// 具体的な担当ハンドラ1
class TechnicalSupport extends SupportHandler {
handle(request) {
if (request.type === "tech") {
return `技術サポートが対応: ${request.message}`;
}
return super.handle(request);
}
}
// 具体的な担当ハンドラ2
class BillingSupport extends SupportHandler {
handle(request) {
if (request.type === "billing") {
return `請求担当が対応: ${request.message}`;
}
return super.handle(request);
}
}
const tech = new TechnicalSupport();
const billing = new BillingSupport();
// チェーンを構築
tech.setNext(billing);
console.log(tech.handle({ type: "billing", message: "支払情報が登録できません" }));
// 請求担当が対応: 支払情報が登録できません
何が便利なのか
要求を送る側と、それを処理する側の結びつきを弱めることができます。ハンドラを追加・削除したり、優先順位を変更したりすることが動的に行えます。
5. Command(コマンド)
使い所
「処理の実行要求」そのものをオブジェクトとして表現し、要求の履歴管理、実行の遅延、あるいは「取り消し(Undo)」機能を実装したい場合に使用します。
使い方とサンプルコード
実行したい処理を execute() メソッドを持つコマンドクラスとしてカプセル化し、呼び出し側と実行する側を切り離します。
// 実際の受信者(処理を実行する機器)
class Light {
turnOn() {
return "明かりがつきました。";
}
turnOff() {
return "明かりが消えました。";
}
}
// コマンド
class LightOnCommand {
constructor(light) {
this.light = light;
}
execute() {
return this.light.turnOn();
}
}
// コマンドを起動する送信者(リモコンなど)
class Switch {
constructor() {
this.history = [];
}
press(command) {
this.history.push(command);
return command.execute();
}
}
const livingLight = new Light();
const lightOn = new LightOnCommand(livingLight);
const remote = new Switch();
console.log(remote.press(lightOn)); // 明かりがつきました。
何が便利なのか
ボタンやメニューなどのUI要素と、実際の処理ロジックを疎結合に保つことができます。コマンドを配列に格納すれば、一括処理(マクロ)やUndoの実装も容易です。
6. Interpreter(インタプリタ)
使い所
特定の文法を持つ簡単な言語や表現式があり、それを評価・解析して実行したい場合に使用します。
使い方とサンプルコード
文法の規則ごとにクラスを作成し、コンテキスト情報を順番に解釈させて処理を進めます。
// 式を解釈するベース
class Expression {
interpret(context) {}
}
// 数値の解釈
class NumberExpression extends Expression {
constructor(number) {
super();
this.number = number;
}
interpret() {
return this.number;
}
}
// 加算の解釈
class AddExpression extends Expression {
constructor(left, right) {
super();
this.left = left;
this.right = right;
}
interpret() {
return this.left.interpret() + this.right.interpret();
}
}
// 簡易式: 5 + 10 の構築
const expr = new AddExpression(new NumberExpression(5), new NumberExpression(10));
console.log(expr.interpret()); // 15
何が便利なのか
文法の変更や新しい構文の追加が、新しい規則クラスを定義するだけで簡単に行えます。ただし、文法が複雑になるとクラス数が膨大になり管理が難しくなるため、あくまで小規模な言語規則に向いています。
7. Mediator(メディエーター)
使い所
多数のオブジェクト同士が複雑にお互いを参照し合い、スパゲッティのような通信状態になってしまっているときに、やり取りを取り持つ仲介役として導入します。
使い方とサンプルコード
オブジェクト同士が直接やり取りするのを禁止し、すべての通信を「メディエーター(調停者)」に集約させ、そこから各オブジェクトへ指示を送るようにします。
// 仲介者(チャットルーム)
class ChatRoom {
showMessage(user, message) {
console.log(`[${user.name}から送信]: ${message}`);
}
}
// 参加者
class Participant {
constructor(name, mediator) {
this.name = name;
this.mediator = mediator;
}
send(message) {
this.mediator.showMessage(this, message);
}
}
const chatRoom = new ChatRoom();
const ken = new Participant("ケン", chatRoom);
const yuki = new Participant("ユキ", chatRoom);
ken.send("こんにちは!"); // [ケンから送信]: こんにちは!
yuki.send("はじめまして。"); // [ユキから送信]: はじめまして。
何が便利なのか
各オブジェクトが他のオブジェクトの存在を意識しなくて済むようになります。オブジェクト間の結合度が下がり、個々のクラスの再利用や改修が非常に楽になります。
8. Memento(メメント)
使い所
オブジェクトの内部状態を一時的に保存しておき、後からその時点の状態にいつでも復元(ロールバック)できるようにしたい場合に使用します。
使い方とサンプルコード
状態を保持する専用の「Memento」オブジェクトを用意します。状態の変更者(Originator)はMementoを生成・復元し、管理者(Caretaker)がそのスナップショットを保管します。
// スナップショットを表す
class Memento {
constructor(state) {
this.state = state;
}
}
// 状態が変化するオブジェクト
class Editor {
constructor() {
this.content = "";
}
type(text) {
this.content += text;
}
save() {
return new Memento(this.content);
}
restore(memento) {
this.content = memento.state;
}
}
const editor = new Editor();
editor.type("第1稿のテキスト。");
const checkpoint = editor.save(); // 状態保存
editor.type("追記したものの余計だったテキスト。");
console.log(editor.content); // 第1稿のテキスト。追記したものの余計だったテキスト。
editor.restore(checkpoint); // 復元
console.log(editor.content); // 第1稿のテキスト。
何が便利なのか
オブジェクトの内部状態を保存・復元する際、そのオブジェクトのカプセル化(プライベートな情報など)を破らずに処理が行える点が最大の特徴です。
9. Observer(オブザーバー)
使い所
あるオブジェクトの状態が変わったときに、それと連動する他の多くのオブジェクトに対して自動的に状態の変化を通知し、更新処理を走らせたいときに最適です。
使い方とサンプルコード
監視対象となる「被験者(Subject)」に、監視者(Observer)をあらかじめ登録(Subscribe)させておきます。状態が変化したタイミングで、リストにある全員の更新メソッドを一斉に呼び出します。
// 監視対象(ニュース配信元)
class NewsProvider {
constructor() {
this.subscribers = [];
}
subscribe(observer) {
this.subscribers.push(observer);
}
notify(news) {
for (const sub of this.subscribers) {
sub.update(news);
}
}
}
// 監視者(読者)
class Reader {
constructor(name) {
this.name = name;
}
update(news) {
console.log(`${this.name}が通知を受け取りました: ${news}`);
}
}
const provider = new NewsProvider();
const taro = new Reader("タロウ");
const hanako = new Reader("ハナコ");
provider.subscribe(taro);
provider.subscribe(hanako);
provider.notify("新しいデザインパターンの本が出版されました。");
// タロウが通知を受け取りました: 新しいデザインパターンの本が出版されました。
// ハナコが通知を受け取りました: 新しいデザインパターンの本が出版されました。
何が便利なのか
配信側と受信側が疎結合になるため、配信元のロジックを修正することなく、通知を受け取りたいオブジェクトを自由に追加・削除できます。
10. State(ステート)
使い所
オブジェクトが持っている「現在の状態」によって、同じメソッドを呼び出したときの挙動をガラリと切り替えたい場合に使用します。
使い方とサンプルコード
状態を表すクラスをそれぞれ定義し、本体オブジェクトが現在持っている「状態オブジェクト」に具体的な処理を委譲します。
// 状態のベース
class State {
write(context) {}
}
// 状態1: 通常時
class NormalState extends State {
write(context) {
console.log("黒い文字でノートに書きます。");
}
}
// 状態2: 重要時
class HighPriorityState extends State {
write(context) {
console.log("赤い文字で大きくノートに書きます。");
}
}
// 本体オブジェクト
class Notebook {
constructor() {
this.state = new NormalState();
}
setState(state) {
this.state = state;
}
write() {
this.state.write(this);
}
}
const note = new Notebook();
note.write(); // 黒い文字でノートに書きます。
note.setState(new HighPriorityState());
note.write(); // 赤い文字で大きくノートに書きます。
何が便利なのか
「if (state === 'normal') else if...」のような、条件分岐だらけの処理を排除できます。状態ごとのロジックが独立したクラスにカプセル化されるため、新しい状態の追加が非常に簡単です。
11. Strategy(ストラテジー)
使い所
目的は同じであるものの、その達成手段(アルゴリズム)が複数存在し、それを状況に応じて動的に差し替えたい場合に使用します。
使い方とサンプルコード
切り替えたいアルゴリズムごとに「戦略(Strategy)」クラスを作成し、コンテキストにそのオブジェクトを差し込んで利用します。Stateパターンと構造は似ていますが、Stateが「内部状態の遷移」に着目するのに対し、Strategyは「処理手段の単純な選択」に着目します。
// 戦略の選択肢1: 高速だが品質が標準的な圧縮
class FastCompression {
compress(file) {
return `[高速圧縮] ${file} を処理しました。`;
}
}
// 戦略の選択肢2: 時間はかかるが高圧縮率
class HighCompression {
compress(file) {
return `[高圧縮率] ${file} を処理しました。`;
}
}
// 圧縮を管理するコンテキスト
class ImageArchiver {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
archive(file) {
return this.strategy.compress(file);
}
}
const archiver = new ImageArchiver(new FastCompression());
console.log(archiver.archive("photo.png")); // [高速圧縮] photo.png を処理しました。
archiver.setStrategy(new HighCompression());
console.log(archiver.archive("photo.png")); // [高圧縮率] photo.png を処理しました。
何が便利なのか
利用する側のプログラムを改変することなく、アルゴリズムの追加や変更が可能になります。単体テストを行う際にも、テスト用のモック戦略に差し替えるのが容易です。
12. Visitor(ビジター)
使い所
オブジェクト構造(ツリーなど)を形成する要素側のクラス群を変更せずに、それらの要素に対して実行したい新しい操作(集計や出力など)を後から追加したい場合に使用します。
使い方とサンプルコード
データを持つ要素クラス側には accept(visitor) という受け入れメソッドだけを用意し、実際の処理は「訪問者(Visitor)」クラス内に記述して処理させます(ダブルディスパッチ)。
// データ要素
class FileElement {
constructor(name) {
this.name = name;
}
accept(visitor) {
visitor.visitFile(this);
}
}
class DirectoryElement {
constructor(name) {
this.name = name;
}
accept(visitor) {
visitor.visitDirectory(this);
}
}
// 訪問者(具体的な操作)
class NamePrinterVisitor {
visitFile(file) {
console.log(`ファイル: ${file.name}`);
}
visitDirectory(dir) {
console.log(`ディレクトリ: ${dir.name}`);
}
}
const file = new FileElement("index.js");
const dir = new DirectoryElement("src");
const printer = new NamePrinterVisitor();
file.accept(printer); // ファイル: index.js
dir.accept(printer); // ディレクトリ: src
何が便利なのか
データ構造を定義するクラス群と、そのデータに対する操作を行うクラス群を綺麗に分離できます。データ構造を変えずに、新しい操作クラスを追加するだけで機能拡張が可能です。
なお、オブジェクトの柔軟な生成手法については「GoF生成に関するデザインパターン5選」、クラス間の関係性を整理する手法については「GoF構造に関するデザインパターン7選」で詳しく解説しています。
13. まとめ:処理の流れをシンプルにするデザインパターン
振る舞いに関する11のパターンは、オブジェクト間の結合度を下げるための多彩なアプローチを提供します。どのパターンも、密結合による変更コストの増大を防ぎ、プログラムのメンテナンス性を高める知恵が凝縮されています。
一度にすべてを使いこなすのは困難ですが、「この処理の条件分岐は多すぎる」「イベントの通知を綺麗に整理したい」と感じた際、これらのパターンが突破口になるはずです。自身のコード設計の道具箱に加えておくのがおすすめです。