Adapter¶
解決する問題¶
既存のクラス(変更できない・したくない)のインターフェースが、こちらの欲しい形と合わない。変換器を挟んで、変更せずに接続したい。
登場人物と責務¶
- Target: クライアントが期待するインターフェース(こちらの語彙)
- Adaptee: 既存の合わないクラス(外部SDK、レガシーコード)
- Adapter: Targetを実装し、内部でAdapteeに変換・委譲する
最小構成図¶
パターンなしの実装¶
// C++20(問題例)— 外部SDKの流儀がゲーム全体に漏れる
void OnExplosion() {
FMOD::Studio::EventInstance* ev = fmodSystem->createEventInstance("event:/explosion");
ev->setVolume(0.8f);
ev->start();
ev->release(); // SDK固有の作法(release必須)を全呼び出し箇所が知る必要がある
}
問題点¶
- SDK固有の型と作法(release、イベントパス形式)が全使用箇所に複製される
- ミドルウェア変更(FMOD→Wwise)で全箇所を書き換え
- SDKなしでのテスト・モック化が不可能(DIP違反そのもの)
パターン適用後のC++コード¶
// C++20
#include <memory>
#include <string>
// Target: ゲーム側の語彙で定義した欲しい形
class IAudioPlayer {
public:
virtual ~IAudioPlayer() = default;
virtual void PlayOneShot(const std::string& soundId, float volume) = 0;
};
// Adapter: SDKの作法をここに閉じ込める
class FmodAudioAdapter : public IAudioPlayer {
public:
explicit FmodAudioAdapter(FmodSystem& system) : system_(system) {}
void PlayOneShot(const std::string& soundId, float volume) override {
auto* ev = system_.createEventInstance(("event:/" + soundId).c_str());
ev->setVolume(volume);
ev->start();
ev->release(); // SDKの作法はこの1か所だけが知っている
}
private:
FmodSystem& system_; // 参照: SDK本体の所有・初期化は起動コードが担う
};
// ゲームコードはTargetしか知らない
// audio->PlayOneShot("explosion", 0.8f);
構造検証用サンプル(SDKをスタブ化): samples/pattern_adapter.cpp
C#またはUnityでの実装¶
// アセットストアのSDKを自分のインターフェースに合わせる
public interface IAdsService { void ShowRewarded(Action<bool> onFinished); }
public class UnityAdsAdapter : IAdsService {
public void ShowRewarded(Action<bool> onFinished) {
// Unity Ads SDKのコールバック形式・列挙型を、自分の形式(bool成功)へ変換
Advertisement.Show("rewardedVideo", new ShowOptions {
resultCallback = r => onFinished(r == ShowResult.Finished)
});
}
}
ゲームでの具体例¶
- ミドルウェアの隔離: サウンド(FMOD/Wwise)、広告、課金、アナリティクス、プラットフォームSDK(実績・セーブ)
- 旧システムの延命: リメイクで旧ゲームロジックを新エンジンのインターフェースに繋ぐ
- 入力: 各プラットフォームの入力APIを共通の
IInputSourceに変換
利点¶
- 変更できないコード同士を、双方無変更で接続できる
- 外部依存が1クラスに隔離され、差し替え・テスト・バージョンアップが局所化する
- 変換ロジック(座標系の違い、単位の違い)の置き場所が明確になる
欠点¶
- 間接層が1枚増える(コードを追うときワンホップ増える)
- Targetの設計を誤る(Adapteeの写しにする)と、隔離の意味がなくなる — Targetは自分の語彙で(DIPの「抽象の所有」と同じ注意)
- Adapteeの機能すべてをTargetに露出しようとすると、Targetが肥大化する(使う分だけ露出する)
適用条件¶
- 変更できない外部コードとの境界(SDK、レガシー、他チームの成果物)
- 差し替えの可能性が現実にある依存
避けるべき条件¶
- 自分で変更できるコード同士 → 直接インターフェースを合わせる方が単純
- 標準ライブラリなど安定していて差し替えない依存 → ラップは追跡コストだけ増える
似たパターンとの違い¶
- Facade: 変換ではなく単純化(多数のクラスを1つの窓口に)。Adapterは既存インターフェース→目的インターフェースの形合わせ
- Decorator: インターフェースを変えずに機能を足す。Adapterは変える
- Proxy: 同じインターフェースのまま、アクセスを制御する
- Bridge: 事後の接続(Adapter)vs 設計時からの分離(Bridge)
実務でよく見かける変形¶
- 双方向Adapter、クラスAdapter(多重継承版。C++では可能だが委譲版が推奨)
- 腐敗防止層 (Anti-corruption layer): Adapterの集合で外部システムの流儀の侵入を防ぐアーキテクチャ用語
過剰設計になる例¶
エンジンAPI(UnityのTransform等)を全部自作インターフェースでラップする「エンジン非依存化」— エンジン移行が実際に計画されていない限り、膨大なAdapter層の保守だけが残ります。境界の選定は「本当に差し替えうるか」で。
関連項目¶
理解度チェック¶
- AdapterとFacadeの目的の違いは?
- Targetを「Adapteeの写し」で設計すると何が失われますか?
- ラップすべき依存とラップ不要な依存の判断基準は?
演習¶
「プラットフォーム実績API」(Steam: SetAchievement(const char*) + StoreStats()、PS: 別作法)を想定し、ゲーム側Target IAchievements を設計してください。「進捗率つき実績」をSteamは%指定、PSは段階指定と仮定した場合の変換の置き場所も決めてください。