プログラミングでシステムを構築する際、オブジェクトの作り方に頭を悩ませたことはありませんか。クラスから単純に new を繰り返すだけでは、機能が追加されたり変更が生じたりした際に、影響範囲が広がりメンテナンスが難しくなることがあります。
オブジェクトを「どのように作るか」という関心事を切り離し、システムをより柔軟にする設計手法。それがGoFデザインパターンのうち「生成に関するパターン(Creational Patterns)」です。今回は、このカテゴリーに分類される5つの主要なパターンについて、具体的な使い所とサンプルコードを交えながら、その利便性を整理していきます。
1. オブジェクト生成をコントロールする意義
プログラムの中でオブジェクトを生成する処理を、あちこちに直接記述してしまうと、システムの柔軟性が失われます。例えば、生成する具体的なクラスを切り替えたくなった場合、すべての new の呼び出し箇所を書き直す必要が生じます。
生成に関するデザインパターンを導入すると、オブジェクト生成の責任を専用の構造に委ねることができます。これにより、クラス同士の結合度が下がり、仕様変更やテストの実施が容易になります。代表的な5つのパターンをそれぞれ詳しく見ていきましょう。
2. Singleton(シングルトン)
使い所
システム全体で「特定のインスタンスを絶対に1つだけに制限したい」場合に使用します。例えば、データベースへのコネクションプール、アプリケーションの設定情報の管理、共通のログ出力機能などが該当します。
使い方とサンプルコード
コンストラクタを外部から呼び出せないように工夫し、唯一のインスタンスを取得するための静的メソッドを提供します。JavaScriptでは、モジュールスコープを利用してインスタンスを1つに絞る手法が一般的です。
class DatabaseConnection {
constructor() {
if (DatabaseConnection.instance) {
return DatabaseConnection.instance;
}
this.connectionString = "mongodb://localhost:27017/mydb";
DatabaseConnection.instance = this;
}
query(sql) {
console.log(`Executing: ${sql} on ${this.connectionString}`);
}
}
// 利用例
const conn1 = new DatabaseConnection();
const conn2 = new DatabaseConnection();
console.log(conn1 === conn2); // true (同じインスタンスを共有している)
何が便利なのか
不要なインスタンスの多重生成によるメモリの無駄遣いを防ぐとともに、複数のインスタンスが乱立してデータの整合性が崩れるリスク(競合)を未然に回避できる点が非常に便利です。
3. Factory Method(ファクトリーメソッド)
使い所
オブジェクトを作成するロジックと、そのオブジェクトを利用するロジックを分離したい場合に使用します。生成されるオブジェクトの具体的な種類が、実行時の状況や設定によって動的に決定されるシーンに適しています。
使い方とサンプルコード
スーパークラスやインターフェース側ではオブジェクトの「生成用メソッド(Factory)」のみを定義しておき、具体的なインスタンス化の決定は、それを継承したサブクラスで行わせます。
// 抽象プロダクトに相当するクラス群
class Dialog {
render() {
const button = this.createButton();
button.onClick();
}
// サブクラスでオーバーライドする生成メソッド
createButton() {
throw new Error("createButton() must be implemented");
}
}
// 具体的なプロダクトの生成(サブクラス)
class WindowsDialog extends Dialog {
createButton() {
return {
onClick: () => console.log("Windowsスタイルのボタンをクリックしました")
};
}
}
class MacDialog extends Dialog {
createButton() {
return {
onClick: () => console.log("Macスタイルのボタンをクリックしました")
};
}
}
// 利用例
const dialog = (process.platform === "win32") ? new WindowsDialog() : new MacDialog();
dialog.render();
何が便利なのか
利用する側のコード(この場合は Dialog クラス)を一切書き換えることなく、新しい種類(例えばLinux向けのダイアログなど)を簡単に追加できるため、機能拡張に対する変更耐性が極めて高くなります。
4. Abstract Factory(アブストラクトファクトリー)
使い所
関連する一連のオブジェクト群(例えば、UI部品の「ボタン」「テキストボックス」「チェックボックス」など)を、特定の「テーマ」(ダークモード、ライトモードなど)に合わせて一貫性を持って生成したい場合に使用します。
使い方とサンプルコード
抽象的な部品群を作るための「抽象的な工場」を定義し、具体的な実装クラスを隠蔽したまま、関連し合うオブジェクトをまとめて生成します。
// 抽象工場
class GUIFactory {
createButton() {}
createCheckbox() {}
}
// 具体的な工場(ライトテーマ用)
class LightThemeFactory extends GUIFactory {
createButton() {
return { paint: () => console.log("白いボタンを描画") };
}
createCheckbox() {
return { paint: () => console.log("白いチェックボックスを描画") };
}
}
// 具体的な工場(ダークテーマ用)
class DarkThemeFactory extends GUIFactory {
createButton() {
return { paint: () => console.log("黒いボタンを描画") };
}
createCheckbox() {
return { paint: () => console.log("黒いチェックボックスを描画") };
}
}
// 利用側の実装
class Application {
constructor(factory) {
this.button = factory.createButton();
this.checkbox = factory.createCheckbox();
}
paint() {
this.button.paint();
this.checkbox.paint();
}
}
// 利用例
const factory = new DarkThemeFactory(); // 一括でダークテーマに設定
const app = new Application(factory);
app.paint();
何が便利なのか
関連し合う部品の組み合わせを誤るミス(例えば、ライトテーマのウィンドウの中にダークテーマのボタンが混ざってしまうこと)を論理的に防ぎつつ、システム全体の見た目や動作ポリシーを一括で切り替えることが可能になります。
5. Builder(ビルダー)
使い所
生成するオブジェクトのパラメータが多く複雑な場合、あるいはオブジェクトを組み立てる手順が段階的に変化する場合に使用します。引数の多いコンストラクタによって、可読性が低下する問題を解決します。
使い方とサンプルコード
オブジェクトを直接生成するのではなく、構成要素を1つずつセットできる「ビルダー」と呼ばれる補助クラスを用意します。最後に build() メソッドを実行することで、完成したオブジェクトを取得します。
class Computer {
constructor() {
this.CPU = "";
this.RAM = "";
this.Storage = "";
this.GPU = "";
}
}
class ComputerBuilder {
constructor() {
this.computer = new Computer();
}
setCPU(cpu) {
this.computer.CPU = cpu;
return this; // メソッドチェーンを可能にする
}
setRAM(ram) {
this.computer.RAM = ram;
return this;
}
setStorage(storage) {
this.computer.Storage = storage;
return this;
}
setGPU(gpu) {
this.computer.GPU = gpu;
return this;
}
build() {
return this.computer;
}
}
// 利用例
const myPC = new ComputerBuilder()
.setCPU("Intel Core i7")
.setRAM("32GB")
.setStorage("1TB SSD")
.setGPU("NVIDIA RTX 4070")
.build();
console.log(myPC);
何が便利なのか
コンストラクタに大量の引数を渡す必要がなくなり、コードが格段に読みやすくなります。また、「メモリは32GBだがGPUは不要」といった個別の構成パターンも、メソッドを呼び分けるだけで柔軟に組み立てられます。
6. Prototype(プロトタイプ)
使い所
新しいオブジェクトを一から作成するコスト(データベースからデータを再取得する、あるいは重い計算処理を行うなど)が非常に高い場合、すでに作成済みのオブジェクトを複製して再利用したいときに適しています。
使い方とサンプルコード
オブジェクト自体に自分自身を複製するメソッド(clone())を実装し、それを使って複製オブジェクトを作成します。
class Enemy {
constructor(name, hp, ap) {
this.name = name;
this.hp = hp;
this.ap = ap;
}
// 自身を複製するメソッド
clone() {
return new Enemy(this.name, this.hp, this.ap);
}
display() {
console.log(`敵: ${this.name} | HP: ${this.hp} | 攻撃力: ${this.ap}`);
}
}
// 利用例
const bossPrototype = new Enemy("魔王", 10000, 250);
// プロトタイプから量産する
const cloneBoss1 = bossPrototype.clone();
const cloneBoss2 = bossPrototype.clone();
cloneBoss2.hp = 8000; // 複製後に一部のステータスだけ調整することも可能
cloneBoss1.display();
cloneBoss2.display();
何が便利なのか
複雑に組み立てられたり、高負荷な初期化処理を行ったりしたオブジェクトを、極めて高速に量産できます。また、具体的なクラス名を知らなくても、手元にあるオブジェクトを複製できるメリットがあります。
なお、オブジェクト同士の構造的な組み合わせについては「GoF構造に関するデザインパターン7選」、オブジェクト間の連携やアルゴリズムの制御については「GoF振る舞いに関するデザインパターン11選」で詳しく解説しています。
7. まとめ:役割に合わせたオブジェクト生成の選択
今回紹介した5つの生成パターンは、いずれも「new という直感的な操作がもたらす密結合」を回避するための具体的な知恵です。小規模なプログラムでは必要性を感じにくいものですが、システムの規模が拡大し、変更要件が重なるにつれて、これらのパターンの真価が発揮されます。
開発者共通の言語としても機能するデザインパターンを設計に組み込み、拡張性の高いクリーンなコードを目指していくのが良さそうです。