デザインパターンの基礎知識
ソフトウェア開発における代表的なデザインパターンを、実例とコードを通じて分かりやすく解説。GoFパターンを中心に、実践的な使い方と適用場面を学びます。
🎯 この記事で学べること
- 1デザインパターンの概念と重要性を理解できます
- 2生成に関するパターン(Singleton、Factory、Builder)を習得できます
- 3構造に関するパターン(Adapter、Decorator、Facade)を理解できます
- 4振る舞いに関するパターン(Observer、Strategy、Command)を学べます
- 5パターンの適切な選択と実装方法を身につけられます
読了時間: 約5分
23個の呪文が世界を変えた
1994年、シリコンバレーのある書店。
4人のプログラマーが書いた1冊の本が、ひっそりと技術書コーナーに並んだ。タイトルは「Design Patterns: Elements of Reusable Object-Oriented Software」。地味な表紙、学術的なタイトル。誰も注目していなかった。
しかし、この本は瞬く間にプログラマーたちの間で話題となった。「これを読むと、コードが見える世界が変わる」「今まで感覚でやっていたことが、理論として整理されている」「まるで魔法の呪文書だ」。
著者の4人、エリック・ガンマ、リチャード・ヘルム、ラルフ・ジョンソン、ジョン・ブリシディーズは、いつしか「Gang of Four(四人組)」と呼ばれるようになった。彼らが提示した23個のパターンは、ソフトウェア設計の共通言語となった。
30年後の今、世界中のプログラマーが「ここはObserverパターンで」「Factoryを使おう」という会話をしている。まるで料理人が「ここは弱火で」「隠し味にバターを」と言うように、設計の定石が共有されている。
この23個のパターンは、なぜ世界を変えたのか?それは、プログラミングを「個人の技」から「共有可能な知識」に変えたからだ。
ストリーミング大手のバグが教えてくれたこと
2008年、ある大手企業はDVDレンタルからストリーミングサービスへの大転換を図っていた。
しかし、深刻な問題に直面していた。新機能を追加するたびに、既存の機能が壊れる。あるエンジニアがレコメンドエンジンを改良すると、なぜか視聴履歴が狂う。別のエンジニアが高画質配信を実装すると、字幕が表示されなくなる。
コードを調査した結果、判明した事実は衝撃的だった。システム全体が巨大なスパゲッティコードになっていた。すべてがすべてに依存し、どこを触っても予期せぬ影響が出る。まるで絡まった釣り糸のようだった。
CTOのエイドリアン・コックロフトは決断した。「システム全体を、デザインパターンを使って再構築する」。
チームは半信半疑だった。学術的な理論が、実際のビジネスで役立つのか?しかし、結果は驚くべきものだった。
Observerパターンを使って、各コンポーネントの依存関係を整理。Strategyパターンで、配信アルゴリズムを切り替え可能に。Decoratorパターンで、動画処理のパイプラインを構築。
1年後、新機能の開発速度は3倍になった。バグは80%減少した。そして何より、新人エンジニアでもコードが理解できるようになった。「まるで地図ができたようだ」とある開発者は語った。
料理とプログラミングの共通点
優れた料理人は、レシピを暗記しているわけではない。
彼らが知っているのは「パターン」だ。煮込み料理なら「香味野菜を炒めて、肉を焼いて、液体を加えて煮込む」。炒め物なら「強火で短時間、油をまわして」。これらのパターンを組み合わせて、無限の料理を生み出す。
プログラミングも同じだ。優れたプログラマーは、すべてのコードを暗記しているわけではない。彼らが知っているのは「設計のパターン」だ。
// 料理:「出汁を取る」というパターン
// 材料は変われど、手順は同じ
// プログラミング:「唯一のインスタンスを保証する」Singletonパターン
class DatabaseConnection {
static instance = null;
constructor() {
if (DatabaseConnection.instance) {
return DatabaseConnection.instance;
}
this.connection = this.connect();
DatabaseConnection.instance = this;
}
connect() {
// データベースへの接続処理
return { /* 接続オブジェクト */ };
}
}
料理人が「今日は鶏で出汁を取ろう」と考えるように、プログラマーは「ここはSingletonパターンを使おう」と考える。材料(実装言語)は変わっても、パターン(設計思想)は変わらない。
建築家クリストファー・アレグザンダーの予言
デザインパターンの起源は、意外にもソフトウェアではなく建築にある。
1970年代、建築家クリストファー・アレグザンダーは革新的な考えを提唱した。「建築には、繰り返し現れる問題と、それに対する解決策のパターンがある」。
例えば「光の入る場所に人は集まる」というパターン。だから、リビングには大きな窓を設ける。「プライベートな空間には段階的に移行する」というパターン。だから、玄関から寝室まで、徐々にプライベート度が上がるよう配置する。
20年後、ケント・ベックとウォード・カニンガムという2人のプログラマーが、この考えをソフトウェアに応用した。「ソフトウェア設計にも、繰り返し現れる問題と解決策があるはずだ」。
そして、Gang of Fourが体系化した。建築のパターンが「住みやすい家」を作るように、ソフトウェアのパターンは「保守しやすいシステム」を作る。
デザインパターンは「レシピ」ではなく「料理の基本技法」です。材料や調味料は変わっても、基本となる調理法は普遍的です。
Singletonパターン:世界に一つだけの存在
東京の地下鉄の運行管理システムを想像してほしい。
もし複数の管理システムが同時に動いていたら、大混乱になる。A システムが「山手線を3分間隔で運行」と指示し、B システムが「5分間隔で運行」と指示したら?電車は衝突し、ダイヤは崩壊する。
これがSingletonパターンが解決する問題だ。「システム全体で、たった一つのインスタンスしか存在してはいけない」という制約を実現する。
class TrainControlSystem {
static #instance = null;
#trains = new Map();
constructor() {
if (TrainControlSystem.#instance) {
throw new Error('既に運行管理システムは起動しています');
}
TrainControlSystem.#instance = this;
}
static getInstance() {
if (!TrainControlSystem.#instance) {
TrainControlSystem.#instance = new TrainControlSystem();
}
return TrainControlSystem.#instance;
}
scheduleTrain(lineId, interval) {
console.log(`${lineId}線を${interval}分間隔で運行設定`);
this.#trains.set(lineId, { interval, lastDeparture: Date.now() });
}
getNextDeparture(lineId) {
const train = this.#trains.get(lineId);
if (!train) return null;
const nextTime = train.lastDeparture + (train.interval * 60 * 1000);
return new Date(nextTime);
}
}
// 使用例
const control1 = TrainControlSystem.getInstance();
const control2 = TrainControlSystem.getInstance();
console.log(control1 === control2); // true - 同じインスタンス
control1.scheduleTrain('山手線', 3);
console.log(control2.getNextDeparture('山手線')); // control1の設定が反映される
現実世界でSingletonが使われる例は多い。データベース接続プール、ログ管理システム、設定マネージャー。すべて「複数存在すると困る」ものだ。
Factoryパターン:自動車工場の生産ラインに学ぶ
日本のある大手自動車メーカーの工場。
同じ生産ラインから、カローラ、プリウス、クラウンが次々と流れてくる。注文に応じて、適切な部品が選ばれ、適切な工程で組み立てられる。工員は車種を意識せず、指示書に従って作業するだけだ。
これがFactoryパターンの本質だ。「何を作るか」と「どう作るか」を分離する。
// 抽象的な製品
class Vehicle {
constructor(model) {
this.model = model;
this.features = [];
}
addFeature(feature) {
this.features.push(feature);
}
describe() {
return `${this.model}: ${this.features.join(', ')}`;
}
}
// 具体的な製品
class ElectricCar extends Vehicle {
constructor(model) {
super(model);
this.batteryCapacity = '75kWh';
}
charge() {
console.log(`${this.model}を充電中...`);
}
}
class HybridCar extends Vehicle {
constructor(model) {
super(model);
this.fuelType = 'ガソリン + 電気';
}
switchMode() {
console.log('EVモードに切り替え');
}
}
class GasolineCar extends Vehicle {
constructor(model) {
super(model);
this.engineSize = '2000cc';
}
refuel() {
console.log('給油中...');
}
}
// Factoryクラス
class AutoFactory {
static createVehicle(type, model) {
let vehicle;
// 車種に応じて適切なクラスをインスタンス化
switch(type) {
case 'electric':
vehicle = new ElectricCar(model);
vehicle.addFeature('自動運転レベル2');
vehicle.addFeature('回生ブレーキ');
break;
case 'hybrid':
vehicle = new HybridCar(model);
vehicle.addFeature('燃費30km/L');
vehicle.addFeature('静音走行');
break;
case 'gasoline':
vehicle = new GasolineCar(model);
vehicle.addFeature('ターボエンジン');
vehicle.addFeature('6速MT');
break;
default:
throw new Error(`Unknown vehicle type: ${type}`);
}
// 共通の装備
vehicle.addFeature('安全システム');
vehicle.addFeature('コネクテッドサービス');
return vehicle;
}
}
// 注文処理
const order1 = AutoFactory.createVehicle('electric', 'EV-X');
const order2 = AutoFactory.createVehicle('hybrid', 'ハイブリッドカー');
const order3 = AutoFactory.createVehicle('gasoline', 'スポーツカー');
console.log(order1.describe());
console.log(order2.describe());
console.log(order3.describe());
// それぞれの車種固有のメソッドも使える
order1.charge();
order2.switchMode();
order3.refuel();
Factoryパターンの美しさは、新しい車種を追加する時に現れる。既存のコードを変更せず、新しいクラスとFactoryの一行を追加するだけ。まるで自動車メーカーが新車種を開発する時、工場全体を作り直す必要がないように。
Observerパターン:SNS革命の裏側
2006年、あるSNSの最初のプロトタイプが動き始めた。
しかし、すぐに問題が発生した。ユーザーAがツイートすると、フォロワーBのタイムラインに反映させる必要がある。単純に考えれば、AがツイートするたびにBのデータベースを更新すればいい。
しかし、Aのフォロワーが100万人いたら?ツイートのたびに100万回のデータベース更新?システムは即座にダウンする。
エンジニアのブレイン・クックは、新聞配達を思い出した。新聞社は購読者リストを持っている。朝刊ができたら、リストの全員に配達する。購読をやめた人はリストから外す。
これがObserverパターンだ。
// SNSの簡略版実装
class User {
constructor(username) {
this.username = username;
this.followers = new Set();
this.timeline = [];
}
// フォロワーを追加(Subscribe)
addFollower(follower) {
this.followers.add(follower);
console.log(`@${follower.username} が @${this.username} をフォローしました`);
}
// フォロワーを削除(Unsubscribe)
removeFollower(follower) {
this.followers.delete(follower);
console.log(`@${follower.username} が @${this.username} のフォローを解除しました`);
}
// ツイートする(Notify)
tweet(message) {
const tweet = {
author: this.username,
message: message,
timestamp: new Date(),
likes: 0,
retweets: 0
};
console.log(`\n@${this.username}: ${message}`);
// 全フォロワーに通知
this.followers.forEach(follower => {
follower.receiveTweet(tweet);
});
}
// ツイートを受信(Update)
receiveTweet(tweet) {
this.timeline.push(tweet);
console.log(` → @${this.username} のタイムラインに追加されました`);
}
// いいねする
like(tweet) {
tweet.likes++;
console.log(`@${this.username} が「${tweet.message}」をいいねしました ❤️ ${tweet.likes}`);
}
}
// 実例:SNS上での情報伝播
const techCEO = new User('techCEO');
const spaceAgency = new User('SpaceAgency');
const rocketCompany = new User('RocketCompany');
const techEnthusiast = new User('TechLover2024');
// フォロー関係の構築
techCEO.addFollower(spaceAgency);
techCEO.addFollower(rocketCompany);
techCEO.addFollower(techEnthusiast);
spaceAgency.addFollower(techEnthusiast);
// ツイートの伝播
techCEO.tweet('Going to Mars is not just possible, it\'s inevitable');
// 宇宙機関もツイート
spaceAgency.tweet('Launch successful! Next stop: the Moon 🚀');
// フォローを解除してみる
techCEO.removeFollower(rocketCompany);
// 解除後のツイート(RocketCompanyには届かない)
techCEO.tweet('Electric vehicle production starting next month');
このパターンの威力は、疎結合にある。発信者は誰がフォローしているか意識せずツイートできる。フォロワーは好きな時にフォロー/アンフォローできる。まさに現実のSNSと同じだ。
Decoratorパターン:スターバックスの注文システム
「トールのキャラメルマキアート、豆乳に変更、ホイップ多め、キャラメルソース追加で」
スターバックスのバリスタは、この複雑な注文を難なく処理する。なぜか?すべての注文が「基本のコーヒー + オプション」という構造になっているからだ。
これがDecoratorパターンの実例だ。基本となるオブジェクトに、機能を動的に追加していく。
// 基本となるコーヒー
class Coffee {
constructor() {
this.description = 'コーヒー';
this.cost = 350;
}
getDescription() {
return this.description;
}
getCost() {
return this.cost;
}
}
// デコレーターの基底クラス
class CoffeeDecorator {
constructor(coffee) {
this.coffee = coffee;
}
getDescription() {
return this.coffee.getDescription();
}
getCost() {
return this.coffee.getCost();
}
}
// 具体的なデコレーター
class Milk extends CoffeeDecorator {
constructor(coffee, type = '通常') {
super(coffee);
this.milkType = type;
}
getDescription() {
return `${this.coffee.getDescription()} + ${this.milkType}ミルク`;
}
getCost() {
const extraCost = this.milkType === '豆乳' ? 60 : 0;
return this.coffee.getCost() + 50 + extraCost;
}
}
class WhipCream extends CoffeeDecorator {
constructor(coffee, amount = '通常') {
super(coffee);
this.amount = amount;
}
getDescription() {
return `${this.coffee.getDescription()} + ホイップ${this.amount}`;
}
getCost() {
const extraCost = this.amount === '多め' ? 30 : 0;
return this.coffee.getCost() + 50 + extraCost;
}
}
class CaramelSyrup extends CoffeeDecorator {
constructor(coffee, extra = false) {
super(coffee);
this.extra = extra;
}
getDescription() {
const sauce = this.extra ? ' + キャラメルソース追加' : '';
return `${this.coffee.getDescription()} + キャラメルシロップ${sauce}`;
}
getCost() {
const extraCost = this.extra ? 50 : 0;
return this.coffee.getCost() + 60 + extraCost;
}
}
// サイズ変更デコレーター
class SizeOption extends CoffeeDecorator {
constructor(coffee, size) {
super(coffee);
this.size = size;
this.sizeFactors = {
'ショート': 0.9,
'トール': 1.0,
'グランデ': 1.3,
'ベンティ': 1.5
};
}
getDescription() {
return `${this.size} ${this.coffee.getDescription()}`;
}
getCost() {
return Math.floor(this.coffee.getCost() * this.sizeFactors[this.size]);
}
}
// 実際の注文
console.log('=== スターバックス注文システム ===\n');
// シンプルなコーヒー
const coffee1 = new Coffee();
console.log(`${coffee1.getDescription()}: ¥${coffee1.getCost()}`);
// カフェラテ(トールサイズ)
const coffee2 = new SizeOption(
new Milk(new Coffee()),
'トール'
);
console.log(`${coffee2.getDescription()}: ¥${coffee2.getCost()}`);
// 複雑な注文:トールのキャラメルマキアート、豆乳、ホイップ多め、キャラメルソース追加
const coffee3 = new SizeOption(
new CaramelSyrup(
new WhipCream(
new Milk(new Coffee(), '豆乳'),
'多め'
),
true // キャラメルソース追加
),
'トール'
);
console.log(`${coffee3.getDescription()}: ¥${coffee3.getCost()}`);
// さらに複雑な注文も可能
const coffee4 = new SizeOption(
new CaramelSyrup(
new WhipCream(
new CaramelSyrup( // ダブルキャラメル
new Milk(new Coffee(), '豆乳'),
false
),
'多め'
),
true
),
'ベンティ'
);
console.log(`${coffee4.getDescription()}: ¥${coffee4.getCost()}`);
Decoratorパターンの美しさは、無限の組み合わせを可能にすることだ。新しいオプション(例:ヘーゼルナッツシロップ)を追加しても、既存のコードに影響しない。まるでレゴブロックのように、組み合わせは自由自在だ。
Strategyパターン:ライドシェアアプリの価格アルゴリズム
金曜日の夜11時、渋谷。雨が降り始めた。
ライドシェアアプリを開くと、普段の3倍の料金が表示される。「サージプライシング」だ。需要と供給のバランスに応じて、価格が動的に変わる。
しかし、価格計算のアルゴリズムは状況によって異なる。通常時、ピーク時、深夜、悪天候、イベント開催時。それぞれ違う計算式を使う。
これをif文の嵐で実装したら、保守不能なコードになる。そこでStrategyパターンの出番だ。
// 価格計算戦略のインターフェース
class PricingStrategy {
calculateFare(distance, duration) {
throw new Error('calculateFareを実装してください');
}
getDescription() {
throw new Error('getDescriptionを実装してください');
}
}
// 通常料金
class StandardPricing extends PricingStrategy {
calculateFare(distance, duration) {
const baseFare = 410; // 初乗り料金
const perKm = 80; // km単価
const perMinute = 50; // 分単価
return baseFare + (distance * perKm) + (duration * perMinute);
}
getDescription() {
return '通常料金';
}
}
// サージプライシング(需要過多時)
class SurgePricing extends PricingStrategy {
constructor(surgeMultiplier) {
super();
this.surgeMultiplier = surgeMultiplier;
}
calculateFare(distance, duration) {
const standardPricing = new StandardPricing();
const baseFare = standardPricing.calculateFare(distance, duration);
return Math.floor(baseFare * this.surgeMultiplier);
}
getDescription() {
return `サージ料金 (${this.surgeMultiplier}倍)`;
}
}
// 深夜料金
class NightPricing extends PricingStrategy {
calculateFare(distance, duration) {
const baseFare = 500;
const perKm = 100;
const perMinute = 60;
const nightSurcharge = 500; // 深夜割増
return baseFare + (distance * perKm) + (duration * perMinute) + nightSurcharge;
}
getDescription() {
return '深夜料金 (22:00-5:00)';
}
}
// 悪天候料金
class BadWeatherPricing extends PricingStrategy {
constructor(weatherCondition) {
super();
this.weatherCondition = weatherCondition;
}
calculateFare(distance, duration) {
const standardPricing = new StandardPricing();
const baseFare = standardPricing.calculateFare(distance, duration);
// 天候による割増率
const multipliers = {
'小雨': 1.2,
'大雨': 1.5,
'台風': 2.0,
'雪': 1.8
};
const multiplier = multipliers[this.weatherCondition] || 1.0;
return Math.floor(baseFare * multiplier);
}
getDescription() {
return `悪天候料金 (${this.weatherCondition})`;
}
}
// ライドシェアコンテキスト
class RideShare {
constructor() {
this.pricingStrategy = new StandardPricing();
this.currentLocation = null;
this.currentTime = null;
this.weather = '晴れ';
}
setPricingStrategy(strategy) {
this.pricingStrategy = strategy;
}
setConditions(location, time, weather) {
this.currentLocation = location;
this.currentTime = time;
this.weather = weather;
// 条件に基づいて自動的に料金戦略を選択
if (time >= 22 || time < 5) {
this.setPricingStrategy(new NightPricing());
} else if (weather !== '晴れ' && weather !== '曇り') {
this.setPricingStrategy(new BadWeatherPricing(weather));
} else if (location === '渋谷' && (time === 21 || time === 22)) {
this.setPricingStrategy(new SurgePricing(2.5));
} else {
this.setPricingStrategy(new StandardPricing());
}
}
estimateFare(from, to, distance, duration) {
const fare = this.pricingStrategy.calculateFare(distance, duration);
console.log('\n=== ライドシェア料金見積もり ===');
console.log(`出発地: ${from}`);
console.log(`目的地: ${to}`);
console.log(`距離: ${distance}km`);
console.log(`予想時間: ${duration}分`);
console.log(`料金タイプ: ${this.pricingStrategy.getDescription()}`);
console.log(`料金: ¥${fare}`);
return fare;
}
}
// シミュレーション
const rideShare = new RideShare();
// ケース1: 平日昼間
rideShare.setConditions('新宿', 14, '晴れ');
rideShare.estimateFare('新宿駅', '東京駅', 8.5, 25);
// ケース2: 金曜夜の渋谷
rideShare.setConditions('渋谷', 22, '晴れ');
rideShare.estimateFare('渋谷駅', '六本木', 3.2, 15);
// ケース3: 雨の日
rideShare.setConditions('品川', 18, '大雨');
rideShare.estimateFare('品川駅', '羽田空港', 15, 30);
// ケース4: 深夜
rideShare.setConditions('銀座', 2, '晴れ');
rideShare.estimateFare('銀座', '世田谷', 10, 28);
Strategyパターンの威力は、新しい料金体系の追加が簡単なことだ。例えば「高齢者割引」や「学生割引」を追加したければ、新しいStrategyクラスを作るだけ。既存のコードには一切触れない。
Commandパターン:Photoshopの「取り消し」魔法
Adobe Photoshopの開発者たちは、1990年代初頭、革命的な機能を実装しようとしていた。
「Ctrl+Z」で直前の操作を取り消す。今では当たり前の機能だが、当時は画期的だった。しかし、実装は困難を極めた。ブラシで描く、色を変える、レイヤーを追加する。無数の操作をどうやって「巻き戻す」のか?
トーマス・ノールは閃いた。「操作そのものをオブジェクトにすればいい」。
これがCommandパターンだ。
// エディタの実装
class PhotoEditor {
constructor() {
this.canvas = {
pixels: [],
layers: [],
currentColor: '#000000',
currentTool: 'brush'
};
this.history = [];
this.currentIndex = -1;
}
// 現在の状態のスナップショット
getState() {
return JSON.parse(JSON.stringify(this.canvas));
}
// 状態の復元
setState(state) {
this.canvas = JSON.parse(JSON.stringify(state));
}
executeCommand(command) {
// 現在位置より後の履歴を削除
this.history = this.history.slice(0, this.currentIndex + 1);
// コマンドを実行
command.execute();
// 履歴に追加
this.history.push(command);
this.currentIndex++;
console.log(`実行: ${command.getName()}`);
this.showStatus();
}
undo() {
if (this.currentIndex < 0) {
console.log('取り消す操作がありません');
return;
}
const command = this.history[this.currentIndex];
command.undo();
this.currentIndex--;
console.log(`取り消し: ${command.getName()}`);
this.showStatus();
}
redo() {
if (this.currentIndex >= this.history.length - 1) {
console.log('やり直す操作がありません');
return;
}
this.currentIndex++;
const command = this.history[this.currentIndex];
command.execute();
console.log(`やり直し: ${command.getName()}`);
this.showStatus();
}
showStatus() {
console.log(`履歴: ${this.currentIndex + 1}/${this.history.length}`);
console.log(`レイヤー数: ${this.canvas.layers.length}`);
console.log('---');
}
}
// コマンドの基底クラス
class Command {
constructor(editor) {
this.editor = editor;
this.previousState = null;
}
execute() {
// サブクラスで実装
}
undo() {
if (this.previousState) {
this.editor.setState(this.previousState);
}
}
getName() {
return this.constructor.name;
}
}
// 具体的なコマンド
class AddLayerCommand extends Command {
constructor(editor, layerName) {
super(editor);
this.layerName = layerName;
}
execute() {
this.previousState = this.editor.getState();
this.editor.canvas.layers.push({
name: this.layerName,
visible: true,
opacity: 100,
blendMode: 'normal'
});
}
getName() {
return `レイヤー追加: ${this.layerName}`;
}
}
class ChangeColorCommand extends Command {
constructor(editor, newColor) {
super(editor);
this.newColor = newColor;
}
execute() {
this.previousState = this.editor.getState();
this.oldColor = this.editor.canvas.currentColor;
this.editor.canvas.currentColor = this.newColor;
}
getName() {
return `色変更: ${this.newColor}`;
}
}
class DrawCommand extends Command {
constructor(editor, x, y, size) {
super(editor);
this.x = x;
this.y = y;
this.size = size;
}
execute() {
this.previousState = this.editor.getState();
this.editor.canvas.pixels.push({
x: this.x,
y: this.y,
size: this.size,
color: this.editor.canvas.currentColor
});
}
getName() {
return `描画: (${this.x}, ${this.y})`;
}
}
// マクロコマンド(複数のコマンドをまとめて実行)
class MacroCommand extends Command {
constructor(editor, commands, name) {
super(editor);
this.commands = commands;
this.name = name;
}
execute() {
this.previousState = this.editor.getState();
this.commands.forEach(cmd => cmd.execute());
}
undo() {
// 逆順で取り消し
for (let i = this.commands.length - 1; i >= 0; i--) {
this.commands[i].undo();
}
}
getName() {
return this.name;
}
}
// 実際の使用例
console.log('=== Photoshop風エディタ ===\n');
const editor = new PhotoEditor();
// 基本的な操作
editor.executeCommand(new AddLayerCommand(editor, '背景'));
editor.executeCommand(new ChangeColorCommand(editor, '#FF0000'));
editor.executeCommand(new DrawCommand(editor, 100, 100, 5));
editor.executeCommand(new AddLayerCommand(editor, 'テキスト'));
// 取り消し
console.log('\n--- Ctrl+Z (取り消し) ---');
editor.undo();
editor.undo();
// やり直し
console.log('\n--- Ctrl+Y (やり直し) ---');
editor.redo();
// マクロ操作
console.log('\n--- アクション実行 ---');
const watermarkMacro = new MacroCommand(
editor,
[
new AddLayerCommand(editor, '透かし'),
new ChangeColorCommand(editor, '#0000FF'),
new DrawCommand(editor, 200, 200, 10),
new DrawCommand(editor, 210, 200, 10),
new DrawCommand(editor, 220, 200, 10)
],
'透かし追加アクション'
);
editor.executeCommand(watermarkMacro);
// マクロも取り消し可能
console.log('\n--- マクロを取り消し ---');
editor.undo();
Commandパターンの美しさは、「操作」と「実行」を分離することだ。操作をオブジェクトとして扱うことで、保存、再生、取り消し、やり直しが可能になる。まるで時間を巻き戻すタイムマシンのように。
パターンの落とし穴:過剰な抽象化
2010年、あるスタートアップでの出来事。
CTOのマイケルは、Gang of Fourの本に感銘を受けた。「我々のシステムをすべてデザインパターンで構築しよう!」。チームは熱狂的に取り組んだ。
3ヶ月後、システムは完成した。AbstractFactoryFactoryProvider、SingletonStrategyObserverProxy、DecoratorAdapterBridgeFacade...。23個すべてのパターンを使い、さらに独自のパターンも追加した。
しかし、誰もコードを理解できなくなった。簡単な機能追加に1週間かかる。バグ修正は迷路を彷徨うようだった。新人エンジニアは3日で辞めた。「哲学書を読んでいるようだ」という言葉を残して。
6ヶ月後、システムは破棄された。ゼロから作り直すことになった。
デザインパターンは薬のようなものです。適切な量なら病を治しますが、過剰摂取は毒になります。シンプルさを保ちながら、必要な場所で適切に使うことが重要です。
現代におけるデザインパターンの進化
2024年、デザインパターンは新しい形で生きている。
ReactのHooksは、Commandパターンの進化形だ。状態の変更を関数として扱う。ReduxはFluxパターンという新しい設計思想を生み出した。マイクロサービスは、パターンをネットワークレベルに拡張した。
// Reactの状態管理:Observerパターンの現代版
import { useState, useEffect } from 'react';
// カスタムフック:状態管理パターンの抽象化
function useWebSocket(url) {
const [messages, setMessages] = useState([]);
const [status, setStatus] = useState('disconnected');
useEffect(() => {
const ws = new WebSocket(url);
ws.onopen = () => setStatus('connected');
ws.onmessage = (event) => {
setMessages(prev => [...prev, JSON.parse(event.data)]);
};
ws.onclose = () => setStatus('disconnected');
return () => ws.close();
}, [url]);
return { messages, status };
}
// Reduxスタイル:Commandパターンの進化
const actions = {
increment: () => ({ type: 'INCREMENT' }),
decrement: () => ({ type: 'DECREMENT' }),
reset: () => ({ type: 'RESET' })
};
const reducer = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT': return state + 1;
case 'DECREMENT': return state - 1;
case 'RESET': return 0;
default: return state;
}
};
AIの時代、デザインパターンの重要性はさらに増している。ChatGPTやCopilotがコードを生成する時、デザインパターンを理解していれば、より良い設計を引き出せる。「ここはObserverパターンで実装して」という指示一つで、整理されたコードが生成される。
未来を設計する力
デザインパターンは、プログラミングの共通言語だ。
建築家が「この部屋には採光窓を」と言えば、職人は理解する。料理人が「野菜は下茹でして」と言えば、見習いは理解する。プログラマーが「ここはFactoryパターンで」と言えば、チームは理解する。
30年前、Gang of Fourが提示した23個のパターンは、今も生きている。形を変え、進化しながら。そして新しいパターンも生まれている。
パターンを学ぶことは、先人の知恵を学ぶこと。車輪の再発明をせず、巨人の肩に乗ること。そして、より良いソフトウェアを、より速く、より確実に作ること。
コードは詩だ。デザインパターンは、その詩の韻律だ。韻律を知れば、美しい詩が書ける。
あなたも、この23個の魔法を使いこなせるようになる。そして、いつか新しいパターンを見つけるかもしれない。それが、ソフトウェア設計の未来を作る。
おすすめコース
関連記事
システムアーキテクチャの基本パターン
システム設計における基本的なアーキテクチャパターンを分かりやすく解説。レイヤードアーキテクチャ、マイクロサービス、MVC、イベント駆動など、実践的なパターンとその適用場面を学びます。
関数型プログラミングの基礎
関数型プログラミングの基本概念を、実例を交えて分かりやすく解説。純粋関数、イミュータビリティ、高階関数など、現代的な開発に必要な考え方を学びます。
オブジェクト指向プログラミングの概念
オブジェクト指向プログラミング(OOP)の基本概念を、実例を交えて分かりやすく解説。カプセル化、継承、ポリモーフィズムを理解し、より良い設計を学びます。