あるサービスを複数の場所で使用したい場合、シングルトンが候補としてありますが、簡便である反面で依存性が隠れてしまう、コードが密結合になるといった問題があります。
DIコンテナの仕組みを利用することで、依存関係を明確にしながらサービスを複数の場所で使用することが出来るようになります。
本記事では簡単なC++のコードとともにシングルトンの代替となるDIコンテナの考え方と仕組みを紹介します。
前提知識
-
C++の基礎文法 -
シングルトンについて理解している
上記の記事でシングルトンについて触れています、まだ理解していない方は本記事の前に読むことを推奨します。
DIコンテナとは
DIコンテナとは、依存関係を外部から注入する仕組み(Dependency Injection)を自動化・管理するための入れ物です。
登録されたサービスの生成方法やライフサイクルをコンテナが管理し、必要なときに依存関係を解決してインスタンスを提供します。
サービスのライフサイクル(Transient / Singleton / Scoped)をシングルトンにすることで、DIコンテナ側がサービスの唯一性を保証することもできます。
これはDIコンテナの利用法の一つにすぎませんが、本記事ではこの用法をコード例として紹介します。
| 項目 | シングルトン | DIコンテナ |
|---|---|---|
| インスタンス管理 | 常に1つ | ライフサイクルを柔軟に設定可能(Transient / Singleton / Scoped) |
| 依存関係の明示性 | 不透明(隠れる) | 明示的に宣言される |
| テスト容易性 | モック差し替え困難 | モック注入が容易 |
| 拡張性 | 制限が多い | 実装切り替えや拡張が容易 |
コード例
ゲームの音量管理をイメージしたコード
//---------------------------------------------------
//! @file SoundManager.h
//! @brief ゲーム内のサウンドを管理するクラスの宣言
//---------------------------------------------------
#pragma once
//---------------------------------------------------
//! @brief ゲーム内のサウンドを管理するクラス
//---------------------------------------------------
class SoundManager {
private:
int volume_;//現在の音量
public:
//---------------------------------------------------
// コンストラクタ
//---------------------------------------------------
SoundManager();
//---------------------------------------------------
// 音量を取得する
//! @return 現在の音量
//---------------------------------------------------
int GetVolume() const;
//---------------------------------------------------
// 音量を設定する
//! @param volume 設定する音量
//---------------------------------------------------
void SetVolume(int volume);
};
#include "SoundManager.h"
//---------------------------------------------------
//! @brief コンストラクタ
//---------------------------------------------------
SoundManager::SoundManager() :
volume_(128) {
}
//---------------------------------------------------
//! @brief 音量を取得する
//---------------------------------------------------
int SoundManager::GetVolume() const {
return volume_;
}
//---------------------------------------------------
//! @brief 音量を設定する
//---------------------------------------------------
void SoundManager::SetVolume(int volume) {
volume_ = volume;
}
//-----------------------------------------------------
//! @file DIContainer.h
//! @brief DIコンテナの実装
//-----------------------------------------------------
#pragma once
#include <memory>
#include <unordered_map>
#include <typeindex>
//-----------------------------------------------------
//! @enum Lifecycle
//! @brief サービスのライフサイクル種別
//-----------------------------------------------------
enum class Lifecycle { Transient, Singleton };
//-----------------------------------------------------
//! @brief DIコンテナクラス
//-----------------------------------------------------
class DIContainer {
private:
std::unordered_map<std::type_index, std::shared_ptr<void>> singletons_; ///< Singleton管理用マップ
std::unordered_map<std::type_index, Lifecycle> lifecycles_; ///< 登録時のライフサイクル
public:
//-----------------------------------------------------
//! @brief サービスを登録する
//! @tparam T 登録する型
//! @param life ライフサイクル(Transient / Singleton)
//-----------------------------------------------------
template<typename T>
void Register(Lifecycle life = Lifecycle::Transient) {
// ライフサイクルを保存
lifecycles_[typeid(T)] = life;
// Singletonの場合はインスタンスを生成して保存
if (life == Lifecycle::Singleton) {
singletons_[typeid(T)] = std::make_shared<T>();
}
}
//-----------------------------------------------------
//! @brief サービスを解決(インスタンスを取得)する
//! @tparam T 解決する型
//! @return インスタンス(ライフサイクルに応じて生成/共有)
//-----------------------------------------------------
template<typename T>
std::shared_ptr<T> Resolve() {
// 登録されているライフサイクルを確認
auto it = lifecycles_.find(typeid(T));
// 登録されている場合
if (it != lifecycles_.end()) {
// Singletonの場合は保存されているインスタンスを返す
if (it->second == Lifecycle::Singleton) {
return std::static_pointer_cast<T>(singletons_[typeid(T)]);
}
// Transientの場合は毎回新しいインスタンスを生成
return std::make_shared<T>();
}
// 未登録の場合はTransient扱い
return std::make_shared<T>();
}
};
#include <iostream>
#include "DIContainer.h"
#include "SoundManager.h"
int main() {
DIContainer container;
// SoundManagerをSingletonとして登録
container.Register<SoundManager>(Lifecycle::Singleton);
// SoundManagerのインスタンスを取得
auto soundManager1 = container.Resolve<SoundManager>();
auto soundManager2 = container.Resolve<SoundManager>();
// 音量を設定
soundManager1->SetVolume(200);
// 2つのインスタンスが同じであることを確認
if (soundManager1 == soundManager2) {
std::cout << "同じインスタンスです" << std::endl;
} else {
std::cout << "別のインスタンスです" << std::endl;
}
std::cout << "現在の音量は" << soundManager1->GetVolume() << std::endl; // Should print 200
return 0;
}
同じインスタンスです
現在の音量は200
ライフサイクルをシングルトンで登録することで、コンテナ内で唯一のインスタンスを作成することに成功しました。
まず、enum classでライフサイクルを列挙します。
//-----------------------------------------------------
//! @enum Lifecycle
//! @brief サービスのライフサイクル種別
//-----------------------------------------------------
enum class Lifecycle { Transient, Singleton };
よく使われるライフサイクルとしては以下のようなものがあります。
| ライフサイクル | インスタンス生成タイミング | 特徴 | 利用例 |
|---|---|---|---|
| Transient |
Resolveするたびに新しいインスタンスを生成 |
軽量で状態を持たないサービスに適する。毎回異なるインスタンスになる | 計算処理、ユーティリティ、フォーマッタ |
| Singleton | 最初の登録時に生成し、以降は同じインスタンスを返す | アプリ全体で唯一のインスタンスを共有。状態を持つサービスに適する | 設定管理、ログ出力、キャッシュ |
| Scoped | スコープ開始時に生成し、スコープ内では同じインスタンスを共有 | スコープが終わると破棄される。リクエストやフレーム単位での管理に適する | Webリクエスト単位のサービス、ゲームのフレーム単位管理 |
本記事では、例としてTransientとSingletonのみを実装しました。
登録処理では、登録した型のライフサイクルを保存し、もしシングルトンで保存されている場合は、インスタンスを生成し、それを連想配列で管理します。
連想配列で管理を行うことで、同じ型が登録された場合にコンテナ内で複数のインスタンスが存在しないようにすることが出来ます。
本記事ではシングルトンを指定して再登録を行った際に上書きを行いますが、Registerの実装次第で上書きの禁止などを行うこともできます。
//-----------------------------------------------------
//! @brief サービスを登録する
//! @tparam T 登録する型
//! @param life ライフサイクル(Transient / Singleton)
//-----------------------------------------------------
template<typename T>
void Register(Lifecycle life = Lifecycle::Transient) {
// ライフサイクルを保存
lifecycles_[typeid(T)] = life;
// Singletonの場合はインスタンスを生成して保存
if (life == Lifecycle::Singleton) {
singletons_[typeid(T)] = std::make_shared<T>();
}
}
DIContainerから必要なクラスを取り出す関数は、依存関係の解決という意味で慣習的にResolve()とします。
取り出したい型のライフサイクルがSingletonなら連想配列のものを、Transientの場合は新しいインスタンスを返すようにします。
//-----------------------------------------------------
//! @brief サービスを解決(インスタンスを取得)する
//! @tparam T 解決する型
//! @return インスタンス(ライフサイクルに応じて生成/共有)
//-----------------------------------------------------
template<typename T>
std::shared_ptr<T> Resolve() {
// 登録されているライフサイクルを確認
auto it = lifecycles_.find(typeid(T));
// 登録されている場合
if (it != lifecycles_.end()) {
// Singletonの場合は保存されているインスタンスを返す
if (it->second == Lifecycle::Singleton) {
return std::static_pointer_cast<T>(singletons_[typeid(T)]);
}
// Transientの場合は毎回新しいインスタンスを生成
return std::make_shared<T>();
}
// 未登録の場合はTransient扱い
return std::make_shared<T>();
}
main.cppを少し編集して、Transientで登録した際の挙動も確認してみようと思います。
#include <iostream>
#include "DIContainer.h"
#include "SoundManager.h"
int main() {
DIContainer container;
// SoundManagerをTransientとして登録
container.Register<SoundManager>(Lifecycle::Transient);
// SoundManagerのインスタンスを取得
auto soundManager1 = container.Resolve<SoundManager>();
auto soundManager2 = container.Resolve<SoundManager>();
// 音量を設定
soundManager1->SetVolume(200);
// 2つのインスタンスが同じか確認
if (soundManager1 == soundManager2) {
std::cout << "同じインスタンスです" << std::endl;
} else {
std::cout << "別のインスタンスです" << std::endl;
}
std::cout << "現在の音量は" << soundManager1->GetVolume() << std::endl; // Should print 200
std::cout << "現在の音量は" << soundManager2->GetVolume() << std::endl; // Should print 128 (default)
return 0;
}
別のインスタンスです
現在の音量は200
現在の音量は128
Transientの場合は別のインスタンスが提供されることを確認できました。
GameManagerがサウンドを鳴らすことをイメージした例
GameManagerがサウンドを鳴らす場合にDIContainerがサービスを渡すコードを書きます。
DIContainer.hには編集を行わないため省略します。
//---------------------------------------------------
//! @file SoundManager.h
//! @brief ゲーム内のサウンドを管理するクラスの宣言
//---------------------------------------------------
#pragma once
//---------------------------------------------------
//! @brief ゲーム内のサウンドを管理するクラス
//---------------------------------------------------
class SoundManager {
private:
int volume_;//現在の音量
public:
//---------------------------------------------------
// コンストラクタ
//---------------------------------------------------
SoundManager();
//---------------------------------------------------
// 音量を取得する
//! @return 現在の音量
//---------------------------------------------------
int GetVolume() const;
//---------------------------------------------------
// 音量を設定する
//! @param volume 設定する音量
//---------------------------------------------------
void SetVolume(int volume);
//---------------------------------------------------
// サウンドを再生する
//! @param soundName 再生するサウンドの名前
//---------------------------------------------------
void PlaySound(const char* sound_name);
};
#include <iostream>
#include "SoundManager.h"
//---------------------------------------------------
//! @brief コンストラクタ
//---------------------------------------------------
SoundManager::SoundManager() :
volume_(128) {
}
//---------------------------------------------------
//! @brief 音量を取得する
//---------------------------------------------------
int SoundManager::GetVolume() const {
return volume_;
}
//---------------------------------------------------
//! @brief 音量を設定する
//---------------------------------------------------
void SoundManager::SetVolume(int volume) {
volume_ = volume;
}
//---------------------------------------------------
//! @brief サウンドを再生する
//---------------------------------------------------
void SoundManager::PlaySound(const char* sound_name) {
//サウンド再生のつもりで、コンソールに音量とサウンド名を出力する
std::cout << "再生 : " << sound_name << " 音量 : " << volume_ << std::endl;
}
//-----------------------------------------------------
//! @file GameManager.h
//! @brief ゲームの進行を管理するクラス
//-----------------------------------------------------
#pragma once
#include <memory>
// 前方宣言
class SoundManager;
//-----------------------------------------------------
//! @brief ゲームの進行を管理するクラス
//-----------------------------------------------------
class GameManager {
private:
std::shared_ptr<SoundManager> sound_manager_; //< サウンド管理サービス
public:
//-----------------------------------------------------
// コンストラクタ
//! @param soundManager サウンド管理サービス(依存関係)
//-----------------------------------------------------
GameManager(std::shared_ptr<SoundManager> sound_manager);
//-----------------------------------------------------
// ゲーム開始処理
//! @note ゲーム開始時にサウンドを鳴らす
//-----------------------------------------------------
void Init();
//-----------------------------------------------------
// ゲーム終了処理
//! @note ゲーム終了時にサウンドを鳴らす
//-----------------------------------------------------
void Exit();
};
#include "GameManager.h"
#include "SoundManager.h"
//-----------------------------------------------------
//! @brief コンストラクタ
//-----------------------------------------------------
GameManager::GameManager(std::shared_ptr<SoundManager> sound_manager)
: sound_manager_(sound_manager) {
}
//-----------------------------------------------------
//! @brief ゲーム開始処理
//-----------------------------------------------------
void GameManager::Init() {
sound_manager_->PlaySound("GameStart");
}
//-----------------------------------------------------
//! @brief ゲーム終了処理
//-----------------------------------------------------
void GameManager::Exit() {
sound_manager_->PlaySound("GameOver");
}
#include <iostream>
#include "DIContainer.h"
#include "SoundManager.h"
#include "GameManager.h"
int main() {
// DIコンテナの生成
DIContainer container;
// SoundManager を Singleton として登録
container.Register<SoundManager>(Lifecycle::Singleton);
// 依存関係を解決してGameManagerを生成
auto soundManager = container.Resolve<SoundManager>();
GameManager controller(soundManager);
controller.Init(); // ゲーム開始処理、サウンド再生
controller.Exit(); // ゲーム終了処理、サウンド再生
return 0;
}
再生 : GameStart 音量 : 128
再生 : GameOver 音量 : 128
必要としている場所に、DIコンテナで解決したインスタンスを渡すことで、複数の場所で使用することが出来ます。
本記事のコードでは、SoundManagerの唯一インスタンスのポインタをGameManager が持つことで、依存関係を明確にしつつ、SoundManagerがゲーム内で複数存在しないことを保証しています。
将来的にSoundManagerをモックに差し替える場合も、DIコンテナ経由で渡すだけなので簡単に行えます。
総括
-
DIコンテナを使用することで、依存性を明確にしつつ、複数の場所でサービスを使用することが可能にとなる。 -
ライフサイクルでシングルトンを選択してDIコンテナに登録することで、複数の場所で唯一のインスタンスを使用できる。