どんなパターン?
- サービスを利用するコードに対して、サービスが実装された具増クラスと結合することなしに、サービスへのグローバルなアクセスポイントを提供するパターン
パターンの要件
- サービスクラスはひとまとまりの操作を抽象インターフェースとして定義する
- 実際にサービスを提供する愚増クラスがこのインターフェースを実装する
- これらとは別にサービスロケータが適切な提供元を探し出して、サービスへのアクセスを提供する
パターンの適用条件
前提として、シングルトンパターンのように、あらゆる箇所からアクセスできるものは問題であるため、使う場面を限定する必要がある
- ログ出力やメモリ管理のように、モジュールの公開APIにすべきでないものを利用する場合
- サウンド出力や、画面表示システムといった、本質的に1つしかありえない機能を利用する場合
使用上の注意
- サービスのありかを突き止めなければならない
- サービスを必要とした時には必ず何らかのサービスを手に入られるように保証する
- サービス側は何によってありかを突き止められたわからない
- 特定の文脈での使用しか想定されていないクラスの場合は、このパターンを使うことを避けたほうが安全
サンプルコード
サービス
サービスは実装のない抽象インターフェースクラスであること
class Audio
{
public:
virtual ~Audio() {}
virtual void playSound(int soundID) = 0;
virtual void stopSound(int soundID) = 0;
virtual void stopAllSounds() = 0;
};
サービスプロバイダ
サービスの実装部分
class ConsoleAudio : public Audio
{
public:
virtual void playSound(int soundID)
{
// ゲーム機のサウンドAPIを使って音を出力する
}
virtual void stopSound(int soundID)
{
// ゲーム機のサウンドAPIを使って音の出力を停止する
}
virtual void stopAllSounds()
{
// ゲーム機のサウンドAPIを使って全ての音の出力を停止する
}
};
ロケータ
サービスとプロバイダの2つを繋ぐクラス
class Locator
{
public:
static Audio* getAudio() { return service_; }
static void provide(Audio* service)
{
service_ = service;
}
private:
static Audio* service_;
};
使い方
プロバイダ登録
ConsoleAudio *audio = new ConsoleAudio();
Locator::provide(audio);
呼び出し
Audio *audio = Locator::getAudio();
audio->playSound(VERY_LOUND_BANG);
NULLサービス
ここまでだとプロバイダ登録前にサービスを使おうとすると、NULLが返されるという問題がある。
そこで、NULLオブジェクトと呼ばれる別のデザインパターンがあり、これを利用すると、オブジェクトを受け取った側は、あたかも「本物」のオブジェクトを受け取ったような安全に実行を継続できる。
そのパターンの導入のためにNULLサービスプロバイダを定義する。
サンプルコード
プロバイダ
これだけ
class NullAudio: public Audio
{
public
virtual void playSound(int soundID)
virtual void stopSound(int soundID)
virtual void stopAllSounds()
};
ロケータ
class Locator
{
public:
static void initialize()
{
service_ = &nullService_;
}
static Audio* getAudio() { return service_; }
static void provide(Audio* service)
{
// NULLサービスに戻す
if (service == NULL) service = &nullService_;
service_ = service;
}
private:
static Audio* service_;
static NullAudio nullService_;
};
ログ・デコレータ
サービスロケータを使ってできる別の改良策「デコレータ」
例として、ログの出力が挙げられる。
サンプルコード
プロバイダ
class LoggedAudio : public Audio
{
public:
LoggedAudio(Audio &wrapped) : wrapped_(wrapped) {}
virtual void playSound(int soundID)
{
log("play sound");
wrapped_.playSound(soundID);
}
virtual void stopSound(int soundID)
{
log("stop sound");
wrapped_.stopSound(soundID);
}
virtual void stopAllSounds()
{
log("stop all sougnds");
wrapped_.stopAllSounds();
}
}
private:
Audio &wrapped_;
使い方
void enableAudioLogging()
{
// 既存のサービスを修飾(デコレート)する
Audio *service = new LoggedAudio(
Locator::getAudio()
);
// 新しいサービスに入れ替える
Locator::provide(service);
}
まとめ・参考情報
- サービスロケータはシングルトンと多くの点で似ているので、どちらの方がニーズに適しているのか比較検討するのが良い
- Unityはこのパターンをコンポーネントパターンと組み合わせて、
GetComponent()
で使っている