BASIC OOP ~Vol.1~

BASIC OOP ~Vol.1~

Singleton

概要

  • インスタンスの唯一性を保証する

解説

Singletonパターンを適用させたいクラスの定義です。
この時点ではSingletonではありません。

//	シングルトンにしたいクラス
class CSingleton final
{
public:
	CSingleton()
		: m_iSerial(0)
	{
	}
	~CSingleton() = default;
public:
	//	シリアルの設定
	void SetSerial(int iSerial)
	{
		m_iSerial = iSerial;
	}
	//	シリアルの取得
	int GetSerial() const
	{
		return m_iSerial;
	}
private:
	int m_iSerial;
};

Singletonにする為には

  • 外部生成の抑制
  • コピーの抑制
  • 唯一インスタンスへの参照メソッド定義

基本はこの3点の対応を行う形になります。

  • デフォルトコンストラクタを隠蔽
  • コピー/ムーブコンストラクタ、代入演算子の削除
  • インスタンスを参照する為の静的メソッド定義

をそれぞれ行えば完了です。

//	シングルトンの対応を行ったクラス
class CSingleton final
{
private:
	//	外部生成抑制:デフォルトコンストラクタを隠蔽
	CSingleton() : m_iSerial(0) {}
	~CSingleton() = default;
public:
	//	コピーの抑制:コピー/ムーブコンストラクタ、代入演算子の削除
	CSingleton(const CSingleton&) = delete;
	CSingleton& operator=(const CSingleton&) = delete;
	CSingleton(CSingleton&&) = delete;
	CSingleton& operator=(CSingleton&&) = delete;
public:
	//	唯一インスタンスへの参照メソッド:静的メソッド
	static CSingleton* GetInstance()
	{
		//	実体はこれ一つだけ
		static CSingleton instance;
		return &instance;
	}
public:
	//	シリアルの設定
	void SetSerial(int iSerial)
	{
		m_iSerial = iSerial;
	}
	//	シリアルの取得
	int GetSerial() const
	{
		return m_iSerial;
	}
private:
	int m_iSerial;
};

実際に利用してみます。

int main()
{
	//	エラー:生成不可
//	CSingleton s;

	//	エラー:生成不可
//	CSingleton* s = new CSingleton();

	//	エラー:コピーコンストラクタ不可
//	CSingleton s(*CSingleton::GetInstance());

	//	エラー:ムーブコンストラクタ不可
//	CSingleton s(std::move(*CSingleton::GetInstance()));

	//	エラー:代入不可
//	CSingleton* s = nullptr;
//	(*s) = *CSingleton::GetInstance();

	//	エラー:代入不可
//	*CSingleton::GetInstance() = *CSingleton::GetInstance();

	//	エラー:ムーブ不可
//	CSingleton* s = nullptr;
//	(*s) = std::move(*CSingleton::GetInstance());

	//	エラー:ムーブ不可
//	std::move(*CSingleton::GetInstance()) = std::move(*CSingleton::GetInstance());

	//	エラー無し
	CSingleton* s = CSingleton::GetInstance();
	s->SetSerial(1234);
	std::cout << s->GetSerial() << std::endl;

	return EXIT_SUCCESS;
}

実行結果

1234

デメリットはグローバル変数の様に利用出来てしまうので
依存性が高まり単体テスト時に弊害が出やすい傾向があります。

リスクを軽減させるならばスーパークラスを用意したり
直接参照での利用を行わない形で設計を行いましょう。

個人的には業務において単体テストへの弊害で苦労したり
問題が発生したケースはあまり記憶に無いです。
(業界や開発環境で事情は異なると思いますが…)

別の方法論としてSingletonパターンの採用を止め
Monostateパターン※1・GoFに含まれないデザインパターン
・インスタンスではなく挙動の唯一性を保証する
を検討するのも良いと思います。

//	Monostateクラス
class CMonostate
{
public:
	CMonostate() = default;
	virtual ~CMonostate() = default;
public:
	virtual void SetSerial(const uint32_t serial)
	{
		m_uSerial = serial;
	}
	virtual uint32_t GetSerial() const
	{
		return m_uSerial;
	}
private:
	static uint32_t m_uSerial;
};
uint32_t CMonostate::m_uSerial = 0;

こちらはインスタンスの唯一性を保証するものではなく
挙動の唯一性を保証するものです。

他にもSingletonパターンの採用ケースとして
NullObjectパターン※2・GoFに含まれないデザインパターン
・操作時に統一性を持たせる為に何もしないオブジェクトを定義する

//	スーパークラス
class IHandle
{
protected:
	IHandle() = default;
public:
	virtual ~IHandle() = default;
public:
	virtual bool IsFinish() const = 0;
};
//	何もしないNullオブジェクト
class CHandle_Null final : public IHandle
{
private:
	CHandle_Null() = default;
public:
	virtual ~CHandle_Null() = default;
public:
	static CHandle_Null& GetInstance() 
	{ 
		static CHandle_Null instance;
		return instance;
	}
public:
	virtual bool IsFinish() const 
	{ 
		return true; 
	}
};

Strategyパターン※3・GoFに含まれるデザインパターン
・オブジェクトを切り替えて挙動を変更する
・関数オブジェクトの思想に近い
との併用も多く見られます。

class CColor final
{
public:
	CColor(float r_ = 0.0f, float g_ = 0.0f, float b_ = 0.0f, float a_ = 1.0f) 
	: r(r_), g(g_), b(b_), a(a_) {}
	~CColor() = default;
public:
	static const CColor& GetWhite()
	{
		static CColor c(1.0f, 1.0f, 1.0f, 1.0f);
		return c;
	}
	static const CColor& GetBlack()
	{
		static CColor c(0.0f, 0.0f, 0.0f, 1.0f);
		return c;
	}
public:
	float r;
	float g;
	float b;
	float a;
};

Singletonパターンについては定型の実装方法を覚えるだけです。
特に難しい部分も無いかと思います。

脚注

↑ 1. ・GoFに含まれないデザインパターン
・インスタンスではなく挙動の唯一性を保証する
↑ 2. ・GoFに含まれないデザインパターン
・操作時に統一性を持たせる為に何もしないオブジェクトを定義する
↑ 3. ・GoFに含まれるデザインパターン
・オブジェクトを切り替えて挙動を変更する
・関数オブジェクトの思想に近い

Tags: