序文:
- 今回はC++でよく使われるデザインパターンであるシングルトンパターンについての関連知識を解説していきます!!
目次
まず知っておくべきことは、デザインパターンとは先人たちのコード開発経験をまとめたものであり、特定の問題を解決するための一連のルーチンであるということです。これは文法的な要件ではなく、コードの再利用性、保守性、可読性、堅牢性、セキュリティを向上させるための一連のソリューションです。
(1) デザインパターンの6原則
単一責任の原則
- クラスの責任は 1 つである必要があり、メソッドは 1 つのことだけを行う必要があります。責任の分担は明確であり、各変更はメソッドまたはクラスの最小単位にまで及びます。
- 使用上の提案: 2 つのまったく異なる関数を 1 つのクラスに配置するのではなく、関連性の高い関数とデータのカプセル化のセットを 1 つのクラスに含める必要があります。
- ユースケース: インターネットチャット: インターネット通信とチャットはネットワーク通信とチャットのカテゴリに分割する必要があります
オープンクローズドの原則
- 拡張の場合はオープン、変更の場合はクローズ
- 使用上の提案: ソフトウェア エンティティに変更を加えるには、修正ではなく拡張機能を使用するのが最善です。
- 使用例: 時間外販売: 製品価格 --- 製品の元の価格を変更せず、新しいプロモーション価格を追加します。
リスコフ置換原理
- 平たく言えば、親クラスが出現できる限り、サブクラスも出現でき、それをサブクラスに置き換えてもエラーや例外は発生しません。
- クラスを継承するときは、必ず親クラスのすべてのメソッドを書き換えてください。親クラスの保護されたメソッドには特に注意してください。サブクラスは、外部呼び出しに対して独自のパブリック メソッドを公開しないようにしてください。
- 使用上の提案: 子クラスは親クラスのメソッドを完全に実装する必要があり、子クラスは独自の個性を持つことができます。親クラスのメソッドをオーバーライドまたは実装する場合、入力パラメータを拡大し、出力を削減することができます。
- ユースケース:ランナークラス・・・走れる、サブクラス長距離ランナー・・・走れる、長距離走が得意、サブクラススプリンター・・・走れる、短距離走が得意。
依存関係逆転の原理
- 高レベルのモジュールは低レベルのモジュールに依存すべきではなく、両方ともその抽象化に依存する必要があります。分割できないアトミック ロジックは低レベルのパターンであり、アトミック ロジックのアセンブリは高レベルのモジュールです。
- モジュール間の依存関係は抽象化 (インターフェイス) を通じて発生し、具象クラス間には直接の依存関係はありません。
- 使用上の提案: すべてのクラスには可能な限り抽象クラスを含める必要があり、具象クラスからクラスを派生させるべきではありません。基本クラスのメソッドをオーバーライドしないようにしてください。リチウム置換の原理と併用してください。
- 使用例: メルセデス ベンツ ドライバー クラス - メルセデス ベンツのみを運転できる; ドライバー クラス - 与えられた車なら何でも運転できる; 車を運転する人: ドライバー - 抽象化に依存する
デメテルの法則、「最も知られていない法則」とも呼ばれる
- オブジェクト間の相互作用を最小限に抑え、クラス間の結合を軽減します。オブジェクトには、他のオブジェクトに関する最小限の知識が必要です。
- クラスの結合度を低くするには明確な要件があります。つまり、直接の友人とのみ通信し、友人間には距離もあります。自分のものは自分のものです (メソッドがこのクラスに配置された場合、クラス間の関係が強化されたり、このクラスに悪影響が及んだりすることはありません。その場合は、このクラスに配置してください)
- 使用例: 教師がクラスリーダーに点呼を取るよう依頼します。教師はクラスリーダーにリストを渡し、クラスリーダーは点呼を完了してチェックし、結果を返します。クラスリーダーが点呼をとる代わりに、教師がチェックします。リスト
インターフェース分離原理
- クライアントは、必要のないインターフェイスに依存すべきではなく、クラス間の依存関係は最小のインターフェイスで確立される必要があります。
- 使用上の提案: インターフェイスのデザインはできるだけシンプルにしてください。ただし、実質的に重要性のないインターフェイスは外部に公開しないでください。
- ユースケース: パスワードを変更するには、ユーザー情報を変更するためのインターフェイスではなく、単一の最小限のパスワード変更インターフェイスが必要であり、データベース操作が公開されるべきではありません。
6 つの設計原則を全体として理解するには、一文で簡潔に要約できます。抽象化を使用してフレームワークを構築し、実装を使用して詳細を拡張します。具体的には、各設計原則は次の注記に対応します。
- 単一責任の原則は、実装クラスが単一の責任を持つ必要があることを示しています。
- 代替の原則は、継承システムを破壊してはならないと教えています。
- 依存関係逆転の原則は、インターフェイス指向のプログラミングを指示します。
- インターフェイス分離の原則は、インターフェイスを設計する際にはシンプルであることが重要であることを示しています。
- ディミットの法則は、結合を減らすように指示します。
- オープンとクローズの原則は、拡張にはオープンであり、変更にはクローズされるという大まかな概要です。
(2) デザインパターンの分類
デザインパターンは、その目的と使用方法に基づいて分類できます。一般的なデザイン パターンの分類は次のとおりです。
-
作成パターン: これらのパターンは、オブジェクトの作成プロセスとオブジェクトのインスタンス化方法に焦点を当てています。一般的な作成パターンには次のようなものがあります。
- シングルトンパターン
- ファクトリーパターン
- 抽象的な工場パターン
- ビルダーパターン
- 試作パターン
-
構造パターン: これらのパターンは、オブジェクトがどのように結合され、相互に関連してより大きな構造を形成するかに焦点を当てています。一般的な構造パターンは次のとおりです。
- アダプターのパターン
- デコレータパターン
- プロキシパターン
- ブリッジパターン
- 複合パターン
- ファサードパターン
- フライウェイトパターン
-
動作パターン: これらのパターンは、オブジェクト間の責任と動作の配分を定義するために、オブジェクト間の通信と相互作用に焦点を当てています。一般的な行動パターンには次のようなものがあります。
- オブザーバーパターン
- 戦略パターン
- テンプレートメソッドパターン
- コマンドパターン
- イテレータパターン
- 状態パターン
- 責任連鎖パターン
- メディエーターパターン
- 訪問者のパターン
- メメントパターン
- インタプリタパターン
これらの主な分類に加えて、実際には、並行モードとスレッド プール モードがあります。
(3) シングルトンモード
この号では、まずデザイン パターンの最初のパターンであるシングルトン パターンを学びます。
1. 定義
クラスは 1 つのオブジェクトのみを作成できます。つまり、シングルトン モードです。この設計パターンにより、システム内にこのクラスのインスタンスが 1 つだけ存在することが保証され、それにアクセスするためのグローバル アクセス ポイントが提供されます。このインスタンスはすべてのプログラム モジュールで共有されます。たとえば、サーバー プログラムでは、サーバーの構成情報がファイルに保存されます。これらの構成データはシングルトン オブジェクトによって均一に読み取られ、サービス プロセス内の他のオブジェクトがこのシングルトン オブジェクトを通じて取得します。この構成情報により、複雑な環境での構成管理。
2. 実施方法
シングルトン モードには通常、遅延スタイル シングルトンとハングリー スタイル シングルトンの 2 つのモードがあります。2 つのモードは次のように実装されます。
1️⃣ レイジーモード
シングルトン オブジェクトの構築に非常に時間がかかる場合や、プラグインの読み込み、ネットワーク接続の初期化、ファイルの読み取りなど、多くのリソースを消費する場合、そのオブジェクトが使用されなくなる可能性があります。プログラムが実行中の場合は、プログラムの先頭で行う必要があります。初期化するだけでは、プログラムの起動が非常に遅くなります。したがって、この場合は遅延モード (遅延読み込み) を使用することをお勧めします。!
遅延モードには 2 つの一般的な設計方法があります。
- a. 静的ポインタ + 使用時に初期化される
- b. ローカル静的変数
(1) Lazy モード実装 1 : 静的ポインタ + 使用時の初期化
template<typename T>
class Singleton
{
public:
static T& getInstance()
{
if (!_value)
{
_value = new T();
}
return *_value;
}
private:
Singleton()
{}
~Singleton()
{}
static T* _value;
};
template<typename T>
T* Singleton<T>::_value = NULL;
【説明する】
シングル スレッドでは、この書き込みメソッドは正しく使用できますが、マルチスレッドでは機能せず、スレッド セーフではありません。
- a. スレッド A とスレッド B が getInstance 関数にアクセスしたい場合、スレッド A は getInstance 関数に入り、if 条件を検出します。初めて入るため、値は空で、if 条件が成立し、オブジェクトがインスタンスを作成する準備ができました。
- b. ただし、スレッド A は OS スケジューラによって中断され、スリープが一時停止され、制御がスレッド B に渡される場合があります。
- c. スレッド B も if 条件になり、スレッド A が構築する前に中断されたため、値がまだ NULL であることがわかりました。このとき、スレッドBはオブジェクトの作成を完了し、スムーズに復帰したものとする。
- d. その後、スレッド A が起動され、オブジェクトを再度作成するために new の実行が継続され、このようにして 2 つのスレッドが 2 つのオブジェクト インスタンスを構築するため、一意性が失われます。
また、メモリリークの問題もあり、新しいものがリリースされることはありませんが、以下はハングリーマン流の改善です。
template<typename T>
class Singleton
{
public:
static T& getInstance()
{
if (!_value)
{
_value = new T();
}
return *_value;
}
private:
// 实现一个内嵌垃圾回收类
class CGarbo
{
public:
~CGarbo()
{
if (Singleton::_value)
delete Singleton::_value;
}
};
static CGarbo Garbo;
Singleton()
{};
~Singleton()
{};
static T* _value;
};
template<typename T>
T* Singleton<T>::_value = nullptr;
【説明する】
- プログラムの最後に、システムはシングルトンの静的メンバー Garbo のデストラクターを呼び出し、シングルトンの唯一のインスタンスを削除します。
このメソッドを使用してシングルトン オブジェクトを解放すると、次のような特徴があります。
- シングルトン クラス内で独自のネストされたクラスを定義する
- リリース専用にシングルトン クラスにプライベート静的メンバーを定義します。
- プログラムの最後でグローバル変数を破棄する機能を使用して、最終的なリリース時間を選択します
【知らせ】
- このコードはマルチスレッド環境ではスレッドセーフではないことに注意してください。
- 複数のスレッドを同時に使用すると
getInstance()
、複数のオブジェクトが作成される可能性があります。スレッドセーフなシングルトン モードを実装するには、ミューテックス ロックや二重チェック ロックなどの適切な同期メカニズムを使用する必要があります(これについては後述します)。
(2) 遅延モード実装 2 : ローカル静的変数
template<typename T>
class Singleton {
public:
static T& getInstance() {
static T instance;
return instance;
}
private:
Singleton() {}
~Singleton() {}
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
};
【説明する】
- 上記のコードでは、コピー コンストラクターと代入演算子の宣言はプライベート セクションに配置されていますが、それらの定義は提供されていません。このようにすると、クラスの外でこれらの関数を呼び出そうとすると、リンク エラーが発生します。この方法は、コピーを制限し、シングルトン オブジェクトの一意性を確保するという目的を達成します。
- このパターンを使用する場合、 クラスのシングルトン オブジェクトは関数
Singleton<YourClass>::getInstance()
を呼び出すこと によってgetInstance()
取得されます 。YourClass
- この実装方法も一般的な遅延モードの実装方法であり、キーワードの使用を必要とせず、
delete
コピーコンストラクタと代入演算子をプライベート化することでコピー動作を制限し、シングルトンの一意性を実現しています。
2️⃣ ハングリーマンモード
プログラムの開始時に、一意のインスタンス オブジェクトが作成されます。シングルトン オブジェクトが決定されているため、マルチスレッド環境に適しており
、マルチスレッドはシングルトン オブジェクトを取得するためにロックする必要がないため、リソースの競合を効果的に回避し、パフォーマンスを向上させることができます。
(1) Hungry Manの実装パターン1 :静的オブジェクトを直接定義する
template<typename T>
class Singleton
{
private:
static T _eton;
private:
Singleton() {}
~Singleton() {}
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static T& getInstance()
{
return _eton;
}
};
template<typename T>
T Singleton<T>::_eton;
アドバンテージ:
- 実装が簡単で、マルチスレッドでも安全です。
欠点:
- a. 複数のシングルトン オブジェクトがあり、これらのシングルトン オブジェクトが相互に依存している場合、プログラムがクラッシュする危険性がある可能性があります。理由: コンパイラにとって、静的メンバー変数の初期化順序と破棄順序は未定義の動作です。
- b. プログラムの先頭でクラスのインスタンスを作成する シングルトン オブジェクトの生成にコストがかかり、めったに使用されない場合、この方法はリソース使用効率の観点から遅延シングルトン クラスよりもわずかに劣ります。ただし、応答時間の観点からは、遅延シングルトン クラスよりもわずかに優れています。
利用条件:
- a. 構築と破壊の依存関係が明らかに存在しない場合。
- b. 頻繁なロックによるパフォーマンスの消耗を避けたい
(2) ハングリーパターン実装 2 : クラス外初期化時の静的ポインタ + 新しい空間の実装
template<typename T>
class Singleton
{
private:
static T* _eton;
Singleton() {}
~Singleton() {}
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static T& getInstance()
{
return *_eton;
}
};
template<typename T>
T* Singleton<T>::_eton = new T();
【まとめ】
- ハングリー モードではプログラムの開始時にシングルトン オブジェクトが作成されるため、遅延読み込みの効果を実現できないことに注意してください。
- プログラムの実行中にシングルトン オブジェクトを必ずしも使用する必要がない場合、不必要なリソースの消費が発生します。
- したがって、通常は遅延パターンがより一般的に使用され、必要な場合にのみシングルトン オブジェクトを作成します。
(4) 遅延モードの安全な実装
class Singleton
{
public:
static Singleton* GetInstance()
{
// 双检查加锁
if (m_pInstance == nullptr) {
m_mutex.lock();
if (m_pInstance == nullptr)
{
m_pInstance = new Singleton;
}
m_mutex.unlock();
}
return m_pInstance;
}
static void DelInstance()
{
m_mutex.lock();
if (m_pInstance)
{
delete m_pInstance;
m_pInstance = nullptr;
}
m_mutex.unlock();
}
// 实现一个内嵌垃圾回收类
class CGarbo
{
public:
~CGarbo()
{
DelInstance();
}
};
// 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
static CGarbo Garbo;
// 一般全局都要使用单例对象,所以单例对象一般不需要显示释放
// 有些特殊场景,想显示释放一下
void Add(const string& str)
{
_vmtx.lock();
_v.push_back(str);
_vmtx.unlock();
}
void Print()
{
_vmtx.lock();
for (auto& e : _v)
{
cout << e << endl;
}
cout << endl;
_vmtx.unlock();
}
~Singleton()
{
// 持久化
// 比如要求程序结束时,将数据写到文件,单例对象析构时持久化就比较好
}
private:
mutex _vmtx;
vector<string> _v;
private:
// 构造函数私有
Singleton()
{}
// 防拷贝
//Singleton(Singleton const&);
//Singleton& operator = (Singleton const&);
static mutex m_mutex; //互斥锁
static Singleton* m_pInstance; // 单例对象指针
};
Singleton* Singleton::m_pInstance = nullptr;
Singleton::CGarbo Garbo;
mutex Singleton::m_mutex;
int main()
{
srand(time(0));
int n = 3000;
thread t1([n]() {
for (size_t i = 0; i < n; ++i)
{
Singleton::GetInstance()->Add("t1线程:" + to_string(rand()));
}
});
thread t2([n]() {
for (size_t i = 0; i < n; ++i)
{
Singleton::GetInstance()->Add("t2线程:" + to_string(rand()));
}
});
t1.join();
t2.join();
Singleton::GetInstance()->Print();
return 0;
}
要約する
以上でシングルトンパターンの説明は終了です。次に、この記事を簡単に振り返ってまとめてみましょう。!!
レイジー モードとハングリー モードはどちらもシングルトン モードの実装であり、クラスのインスタンスが 1 つだけであることを保証し、グローバル アクセス ポイントを提供するために使用されます。
- 遅延モード: 最初に使用されるときのみオブジェクト インスタンスを作成することを指します。具体的な実装としては、通常 getInstance() メソッド内で遅延ロードにより遅延モードを判定し、インスタンスがまだ作成されていない場合は必要に応じてインスタンスを作成して返します。遅延モードの利点は、リソースを節約し、必要な場合にのみインスタンスを作成することですが、欠点は、スレッドの安全性を確保するためにマルチスレッド環境で追加の同期メカニズムが必要になることです。
- ハングリー モード: クラスがロードされるときにオブジェクト インスタンスを作成することを指します。具体的な実装に関しては、Hungry Pattern はクラスの静的メンバー変数にインスタンス オブジェクトを直接作成し、getInstance() メソッドでインスタンスを返します。ハングリーマン モードの利点は、実装が簡単で、マルチスレッド同期の問題を考慮する必要がないことですが、欠点は、アプリケーションの起動時にインスタンスが作成されるため、一部のリソースが無駄になる可能性があることです。
実際のアプリケーションでは、状況に応じて怠け者モードと空腹者モードの長所と短所を比較検討し、適切な実装方法を選択してください。
以上がこの記事の全内容です、ご視聴、応援してくださった皆様、誠にありがとうございました!!!