従来のOOPの欠点を取り除き、モジュラースタイルでC ++で記述します。
運命によって、私はC ++で記述された中程度の複雑さのプロジェクトをサポートおよび開発する必要がありました。 このプロジェクトは、古典的なOOPスタイルで記述されており、モジュールとクラスによって適切に構成されています。 その前に、JavaとApache Tapestry 5でプロジェクトを開発するのに多くの時間を費やしたと言わなければなりません。特に、IOCコンテナーのイデオロギーを非常によく理解しました。 したがって、いくつかのアイデアがそこからコピーされます。
そのため、プロジェクトは構造化されていますが、ほとんどすべてのヘッダーファイルに小さな変更を加えると、プロジェクトの半分が再コンパイルされます。 コードを記述するときの構文の詳細には特に注意していません(ヘッダー、名前空間などを含めるのを忘れることは私にとっては普通です)。 したがって、私は共有したいコンポーネントコードの接続性を減らすために、プロジェクトにいくつかのプラクティスを実装することにしました。 すぐに警告を出したいです。 このプロジェクトにはC ++ 98との互換性が必要なため、その範囲を超えるものはすべてBoostを使用して実装されます。
可変寿命
OOPの基本原則の1つはカプセル化です。 これには、変数が使用されている場合にのみ変数を使用可能にするというルールが含まれています。 可用性は、自動変数の寿命とほぼ同等です。 したがって、タイプ
MyStack
変数がクラスAのプライベートメンバーである場合、クラスのすべてのユーザーはヘッダーMyStack.hのインポートも強制されます。 この変数が1つの関数のみで使用され、状態が含まれていない場合は、一般に静的変数にする必要があります。 さらに、ブロックの最後まで自動変数が存在することを忘れないでください。これを使用して、コードブロックにかっこを追加することで、不要な変数をさらに破棄します。
PImpl
クラスのプライベート部分の実装を隠す問題は、実装ポインタ(Pimpl)によって部分的に解決されます。 このテーマに関する記事は十分にあるので、Pimmplについて詳しく説明したくありません。 たとえば、サッターの紋章:
コメントを作成し、イディオムの実装のみを行います。
- このイディオムは、実装用のパラメーターを受け取るパブリックコンストラクターを隠しません。 この問題は、オブジェクトのインターフェイスとファクトリを組み合わせることで解決できます。
- モジュールのインクルードのパブリック部分に不要なものをすべて、実装とともに移動することを忘れないでください。
- 余分なコードを目から隠すために、C ++ 98と互換性のあるPImplモジュールを実装しました
コード#ifndef PIMPL_H #define PIMPL_H ///idea from GotW #101: Compilation Firewalls, Part 2s http://herbsutter.com/gotw/_101/ #include <boost/scoped_ptr.hpp> template<typename T> class PImpl { private: boost::scoped_ptr<T> m; public: PImpl() : m(new T) { } template<typename A1> PImpl(A1& a1) : m(new T(a1)) { } // 2 9 …. template<typename A1, typename A2, typename A3, typename A4, typename A5, typename A6 , typename A7, typename A8, typename A9, typename A10> PImpl(A1& a1, A2& a2, A3& a3 , A4& a4, A5& a5, A6& a6, A7& a7, A8& a8, A9& a9, A10& a10) : m(new T(a1, a2, a3, a4, a5 , a6, a7, a8, a9, a10)) { } PImpl(const PImpl& orig) : m(new T(*orig)) { } T* operator->() const { return m.get(); } T& operator*() const { return *m.get(); } PImpl& operator=(const PImpl& orig) { m.reset(new T(*orig)); return *this; } }; #endif /* PIMPL_H */
- すべてのクラスで、実装宣言は次のようになります
class Impl; PImpl<Impl> me;
me
はVBAから借りました
- (パブリックメソッドを呼び出すために)パブリック部分へのポインターが必要な場合、パブリック
this
は最初のパラメーターImpl
コンストラクターに渡され、ppub
フィールドに格納されます
- 完全な宣言を持つ実装は、現在のモジュールにのみスコープがあるため、常に
struct
として宣言されます。
- 通常、実装には、コンストラクターと、パブリックコンストラクターを完全に繰り返すオーバーロードオペレーターが必要です。 コピーコンストラクタと
operator=
については、me
とppub
設定することを忘れないでください。
- JavaスタイルのImpl関数宣言。 ご存知のように、クラスですぐに宣言および定義された関数はインライン関数です。 インラインはコンパイラーへの単なるアドバイスであり、考慮に入れていない可能性があることを忘れてはなりません。したがって、大規模な関数はインライン化されない可能性が高くなりますが、関数の宣言と定義の定型文は少なくなります。
- 単体テストについて。 ご存じのように、ユニットテストでは、テスト対象のモジュールが依存する実装の代わりにスタブが必要になることがよくあります。 コードが依存するオブジェクトが
PImpl
で実装されている場合、リンカーを使用して実際の実装をスタブに非常に簡単に置き換えることができます。 隠された実装のテストは、#includeディレクティブを使用してテストモジュールに実装コードを含めることで可能です。
上記の包括的な例------- Hasher.h ------ #include <PImpl.h> class Hasher { class Impl; // class struct PImpl<Impl> me; // public: Hasher(); void execute(); int getResults(); }; ------- Hasher.cpp ------ #include “Hasher.h” #include <HashLib.h> #include “SecTokens.h” // . struct struct Hasher::Impl { Hasher* ppub; // HashContext cnt; int hash; Impl(Hasher* ppub): ppub(ppub) { } void prepare() { HashLib::createContext(cnt); hash = 0; } void update(int val) { HashLib::updateHash(cnt, hash, val); } void finalize() { HashLib::releaseContext(cnt); } }; Hasher::Hasher(): me(this) { // } void Hasher::execute() { me->prepare(); me->update(SecTokens::one); me->update(SecTokens::two); me->finalize(); } int Hasher::getResults(){ return me->hash; } ------- Cryptor.h ------ #include <string> #include <PImpl.h> class Cryptor { class Impl; PImpl<Impl> me; public: Cryptor(std::string salt); std::string crypt(std::string plain); }; ------- Cryptor.cpp ------ #include <CryptoLib.h> #include “Cryptor.h” struct Cryptor::Impl { std::string salt; CryptoContext cnt; Impl(std::string salt): me(salt) { } void prepare() { CryptoLib::createContext(cnt); } void update(std::string plain) { CryptoLib::updateHash(cnt, plain); } std::string finalize() { return CryptoLib::releaseContext(cnt); } }; Cryptor::Cryptor(std::string salt): me(salt) { } std::string Cryptor::crypt(std::string plain) { me->prepare(); me->update(plain); return me->finalize(); } ------- MockHasher.cpp ------ #include “Hasher.h” struct Hasher::Impl { }; void Hasher::execute() { } int Hasher::getResults(){ return 4; } ------- TestCryptor.cpp ------ #include “Cryptor.cpp” int main(int argc, char** argv) { Cryptor::Impl impl(“salt”); impl.prepare(); // impl prepare impl.update(“text”); // impl update std::string crypto=impl.finalize(); // crypto }
したがって、テストを記述する必要があるCryptoLib
クラス(特定のHashLib
ラッパー)と、Cryptor
依存するHasher
クラス(特定のHashLib
ラッパー)があります。 ただし、Cryptor
依然としてHashLib
およびSecTokens
に依存しており、Cryptor
テストでは絶対にこれを必要としません。 代わりに、MockHasher.cppを準備します。
Cryptor.cppコードはTestCryptor.cppに含まれているため、テストをビルドするには、TestCryptor.cppとMockHasher.cppのみをコンパイルして構成します。 これはこの記事のトピックではないため、ユニットテストライブラリに基づいた例を示しません。
ヘッダーファイルの包含の改訂
ここでは簡単です。 ヘッダーは、コードの分析中にできるだけ遅く含める必要がありますが、できればファイルの先頭に含める必要があります。 つまり クラス実装のみがサードパーティヘッダーを使用する場合、クラスヘッダーからクラス実装モジュールに転送します。
パブリック関数の代わりにコールバックとファンクター
このプロジェクトには、プラットフォームに依存するすべての機能を作成するモジュールがあります。
Platform
と呼ばれ
Platform
。 同じ
platform
名前空間で宣言した関連のない関数を持つモジュールが判明しました。 将来的には、プラットフォームに応じてモジュールを実装に置き換えます。 しかし、ここに問題があります。 関数の1つは、一般に別のパブリッククラス
Settings
プライベート部分で宣言されたクラスの<key、value>ペア(これは
std::map
ですが、特定のコンパレーターを使用)を埋める必要があります。
プライベートクラスをパブリックにし、プラットフォームヘッダーを複数のヘッダーに分割できます。 この場合、fill関数は、このfillに関連しないクラスには含まれず、この
std::map
依存しなくなり
std::map
。 テンプレートコンパレータのスコープをプライベートからより一般的なものに変更すると、コンポーネントの接続性が向上することを除いて、私はヘッダーファイルの作成を支持しません。 変更すると、プラットフォーム固有のプレースホルダーに依存するすべてが再コンパイルされます。
別の方法は、
boost::bind
およびcallback関数を使用する
boost::bind
です。 プレースホルダー関数は関数へのポインターを取ります
void fillDefaults(boost::function<void(std::string, std::string) > setDefault);
の代わりに
void fillDefaults(std::map<std::string, std::string, ci_less>& defaults);
Settings
のプライベート部分にコールバックを作成します。
void setDefault(std::string key, std::string value) { defaults[key] = value; } void fillDefaults() { platform::fillDefaults(boost::bind(&SettingsManager::Impl::setDefault, this, _1, _2)); }
の代わりに
void fillDefaults() { platform::fillDefaults(defaults); }
pimplを使用すると、同じ名前のプライベートのラッパーの形式でパブリック関数を作成する方が便利な場合があります。 上記の関数を使用する
void Hasher::execute() { me->prepare(); me->update(SecTokens::one); me->update(SecTokens::two); me->finalize(); }
として想像することができます
void Hasher::Impl::execute() { prepare(); update(SecTokens::one); update(SecTokens::two); finalize(); } void Hasher::execute() { me->execute(); }
しかし、バインドファンクタでこれを行うことができます
------- Hasher.h ------ #include <boost/functions.hpp> #include <PImpl.h> class Hasher { class Impl; // class struct PImpl<Impl> me; // public: Hasher(); boost::function<void()> execute; int getResults(); }; ------- Hasher.cpp ------ //……... Hasher::Hasher(): me(this), execute(boost::bind(&Hasher::Impl::execute, &*me)) { } int Hasher::getResults(){ return me->hash; }
関数定義を取り除きました
これで、以前のようにexecuteを呼び出すことができます。
void f(Hasher& h) { h.execute(); }
そして、例えば、実行のために別のパフォーマーに送られます
void f(Hasher& h, boost::asio::io_service& executor) { executor.post(h.execute); }
の代わりに
void f(Hasher& h, boost::asio::io_service& executor) { executor.post(boost::bind(&Hasher::execute, &h)); }
ラッパー関数のボイラープレート宣言は、ブーストファンクター宣言のボイラープレート宣言に変換され、コンストラクターのみに残りました。
コインには裏返しがあることに注意してください。
execute
クラスのパブリックフィールドになり、実行時に新しい値が誤って割り当てられる可能性がありますが、これは関数では発生しません。 また、仮想メソッドの通常のオーバーライドは使用できなくなりましたが、この問題は簡単に解決できます。
したがって、JavaScriptのような高階関数の魅力が得られます。
メイントピックの範囲を超えたファンクターについてのもう少しの言葉。 ファンクタを作成し、それに基づいて別のファンクタをより少ない引数で作成したいとします
void myFunction(int, int); int main(int argc, char** argv) { boost::function<void(int, int)> functor1(boost::bind(myFunction, _1, _2)); boost::function<void(int)> functor2(boost::bind(functor1, 4, _1)); }
boost :: bind(functor1、4、_1)のこの呼び出しは、目を痛めます。 関数ポインタとバインドを組み合わせて使用しないでください。これらは別々に使用されることはめったにありません。 次に、上記のコードは次の形式を取ります。
int main(int argc, char** argv) { Bindable<void(int, int)> functor1(boost::bind(myFunction, _1, _2)); Bindable<void(int)> functor2(functor1.bind(4, _1)); }
バインド可能なコード
#ifndef BINDABLE_H #define BINDABLE_H #include <boost/bind.hpp> #include <boost/function.hpp> template<typename Signature> struct Bindable : public boost::function<Signature> { Bindable() { } template<typename T> Bindable(const T& fn) : boost::function<Signature>(fn) { } template<typename NewSignature, typename A1> Bindable<NewSignature> bind(const A1& a1) { return boost::bind(this, a1); } // 2 9 template<typename NewSignature, typename A1, typename A2, typename A3, typename A4, typename A5, typename A6, typename A7, typename A8, typename A9, typename A10> Bindable<NewSignature> bind(const A1& a1, const A2& a2, const A3& a3, const A4& a4, const A5& a5, const A6& a6, const A7& a7, const A8& a8, const A9& a9, const A10& a10) { return boost::bind(*this, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10); } }; #endif /* BINDABLE_H */
コンストラクターオプションの隠蔽
最初に、デザイナーパラメーターのタイプを決定する必要があります。
- 特定のインスタンスアプリケーションの構成パラメータ。 通常、これらはフラグ、ライン、メトリックなどの単純なタイプのパラメーターです。 ただし、これらのオプションを非表示にすることはできません。
- 実装作業のグローバルスコープから取得したオブジェクト。 ここでそれらを非表示にします。
「いつでもアクセスできるのに、なぜグローバルにアクセス可能なオブジェクトをコンストラクターに転送するのか?」という疑問が生じるかもしれません。 はい、そうです。 しかし、そうしないほうが良い理由はいくつかあります。
- グローバルオブジェクトの取得はリソースを大量に消費する操作になる場合があるため、クラスフィールドにキャッシュすることをお勧めします
- グローバルオブジェクトの取得は、たとえば
globalStorage.getProfiles().getProfile(“Default”)
ような複雑な構文を持つことができます。 このような式を繰り返さないためには、オブジェクトまたはその参照をクラスフィールドに保存することをお勧めします
- グローバルオブジェクトのコピーを変更する必要がある場合があります。 次に、コピーもクラスフィールドにある必要があります
- デバッグ目的で使用済みオブジェクトを置き換える必要がある場合があります。 次に、クラスフィールドへの抽出と割り当ての呼び出しが1回だけ変更されます。
継承 工場とインターフェース
インターフェイスとして絶対抽象クラスを使用し(ヘッダーファイルで十分)、必要なコンストラクターパラメーターを使用して継承者を作成すると、パラメーターの公開を回避できます。 この場合、ファクトリを使用してインスタンスを作成します。 これは、インターフェイスで宣言され、実装モジュールで定義されたファクトリメソッド、またはオブジェクトが新しいオブジェクトまたは新しいオブジェクトへのポインタを返す独立したクラスです。
長い間、私は、継承または構成を使用できる場合、構成を選択するという事実に傾倒しています。 さらに、Pure Virtual Function Calledエラーを受け取ることで、このアプローチの正確性を確信しました。
構成
pimplイディオムがクラスに実装されている場合、プライベート実装を作成するときに、パブリック部分のコンストラクターパラメーターではなく、グローバルスコープのオブジェクトにコンストラクターに渡すことができます。 つまり パブリックコンストラクターにはグローバルパラメーターはなく、フラグのみがあります。 インスタンスを作成するコードセクションで実際に知って設定する必要があるパラメーター。
ファイルの構造化、モジュール化、遅延初期化
プロジェクトには、約50個の「.cpp」ファイルとヘッダーファイルが含まれています。 ファイルは論理的にディレクトリ(サブシステム)に分割されます。 コードには、単純型のグローバル変数と、ユーザー型の共有オブジェクトにアクセスするためのオブジェクトが含まれています。 オブジェクトへのアクセスは次のようになります
globalStorage.getHoster()->invoke();
または:
Profile pr=globalStorage.getProfiles()->getProfile(“Default”);
上記の
Platform
同様に、
globalStorage
を使用する
globalStorage
、すべての外部タイプで
globalStorage
インターフェースをエクスポートするものを知る
globalStorage
ます。 ただし、
GlobalStorage
は特定のタイプ(または特定のインターフェイスを実装する)のオブジェクトを実際に返す必要があり、
Platform
ような問題を解決する方法はありません。
したがって、次の目標は、サブシステムをApache Tapestry 5 IOCモジュールに似たものに変換し、グローバルオブジェクト(以降、サービスはTapestryサービスに似ています)へのアクセスを簡素化し、サービスの構成をIOCモジュール内の別のファイルに転送することです。 その結果、実際のコンポーネントを取得します( コンポーネント指向プログラミングを参照)
すぐに言いたいのは、本格的なIOCコンテナのことではないということです。 説明した例は、シングルトンサービステンプレートとファクトリの一般化のみです。 このアプローチを使用して、 シャドウサービス (独立したサービスとしてサービスフィールドを表します)およびその他のサービスソースを実装することもできます。
IOCモジュールサービスの構成
作成する
IOC.hヘッダー
#include "InjectPtr.h" ///Helper interface class. Only for visual marking of needed methods. ///We can't do virtual template members namespace ioc { ///methods like http://tapestry.apache.org/defining-tapestry-ioc-services.html#DefiningTapestryIOCServices-ServiceBuilderMethods ///Like public @InjectService or @Inject annotation ///ServiceId Case http://tapestry.apache.org/defining-tapestry-ioc-services.html#DefiningTapestryIOCServices-ServiceIds template<typename T, size_t ID> InjectPtr<T> resolve(); ///Singleton or factory case template<typename T> InjectPtr<T> resolve(); };
代わりに今
boost::shared_ptr<Hoster> hoster = globalStorage.getHoster();
コールは次のようになります
InjectPtr<Hoster> hoster = ioc::resolve<Hoster>();
ご覧のとおり、この設計では余分なものはインポートされません。 コードで
Hoster
を取得する必要がある場合は、自分でヘッダーをインポートするように注意する必要があります。
resolve
メソッドテンプレートの2番目のパラメーターは、サービス識別子です。 1つのインターフェースを持つ複数のサービスがある場合に使用されます。
InjectPtr
は、遅延(遅延)初期化を伴うオブジェクトへのスマートポインターです。
boost::shared_ptr
on
boost::shared_ptr
を保存されたオブジェクトに内部的に保存します。 後者は、
InjectPtr
最初に参照解除されるときに初期化されます。
InjectPtr
はファクトリファンクタを受け取り、保存されたオブジェクトのインスタンスを作成します。
InjectPtrコード
#ifndef INJECT_PTR_H #define INJECT_PTR_H #include <cassert> #include <cstddef> #include <boost/shared_ptr.hpp> #include <boost/scoped_ptr.hpp> #include <boost/make_shared.hpp> #include <boost/function.hpp> #include <boost/thread/mutex.hpp> ///Pointer to lazy instantiative object template<typename T> class InjectPtr { private: typedef boost::function<T*() > Factory; boost::shared_ptr< boost::shared_ptr<T> > px; boost::shared_ptr< boost::scoped_ptr<boost::mutex> > instantiateMutex; Factory factory; public: ///Main constructor. Take factory for future instantiate object InjectPtr(Factory factory) : px(boost::make_shared<boost::shared_ptr<T> >()) , instantiateMutex(boost::make_shared<boost::scoped_ptr<boost::mutex> >(new boost::mutex)) , factory(factory) { } InjectPtr() : px(boost::make_shared<boost::shared_ptr<T> >()) , instantiateMutex(boost::make_shared<boost::scoped_ptr<boost::mutex> >()) { } InjectPtr(boost::shared_ptr<T> pObject) : px(boost::make_shared<boost::shared_ptr<T> >(pObject)) { assert(*px != 0); } InjectPtr(InjectPtr const &orig) : px(orig.px) , instantiateMutex(orig.instantiateMutex) , factory(orig.factory) { } InjectPtr & operator=(InjectPtr const & orig) { px = orig.px; instantiateMutex = orig.instantiateMutex; factory = orig.factory; return *this; } virtual ~InjectPtr() { } T & operator*() { instantiate(); return **px; } T * operator->() { instantiate(); return &**px; } bool operator!() const { return !*px; } void operator==(InjectPtr const& that) const { return *px == that->px; } void operator!=(InjectPtr const& that) const { return *px != that->px; } boost::shared_ptr<T> sharedPtr() { instantiate(); return *px; } void instantiate() { if (!*px && factory) { { boost::mutex::scoped_lock lock(**instantiateMutex); if (!*px) { px->reset(factory()); } } instantiateMutex->reset(); } } Factory getFactory() const { return factory; } void setFactory(Factory factory) { if(!*px && !this->factory){ if(!*instantiateMutex) instantiateMutex->reset(new boost::mutex); this->factory = factory; } } }; template<class T, class U> InjectPtr<T> static_pointer_cast(InjectPtr<U> r) { return InjectPtr<T>(boost::static_pointer_cast<T>(r.sharedPtr())); } #endif /* INJECT_PTR_H */
InjectPtr
スレッドセーフです。 オブジェクトの作成中、操作はミューテックスによってブロックされます。
IOC構成ファイルに移動します。
ioc::resolve
を完全に特殊化する
ioc::resolve
テンプレートメソッドを
ioc::resolve
コード
------- IOCModule.h ------ // #ifndef IOCMODULE_H #define IOCMODULE_H #include <boost/functional/factory.hpp> #include <boost/bind.hpp> #include <IOC.h> #endif /* IOCMODULE_H */ ------- IOCModule.cpp ------ #include "Hoster.h" #include "SomeService.h" #include "InjectPtr.h" #include <IOCModule.h> #include <IOC.h> //Module like http://tapestry.apache.org/tapestry-ioc-modules.html //Now only for: - To provide explicit code for building a service using namespace ioc; ///methods like http://tapestry.apache.org/defining-tapestry-ioc-services.html#DefiningTapestryIOCServices-ServiceBuilderMethods template<> InjectPtr<SomeService> resolve<SomeService>() { static InjectPtr<Hoster> result(boost::bind(boost::factory<SomeService*>())); return result; } ///Hoster takes SomeService in constructor template<> InjectPtr<Hoster> resolve<Hoster>() { static InjectPtr<Hoster> result(boost::bind(boost::factory<Hoster*>(), resolve<SomeService>())); return result; }
GCCは、関数の静的ローカル変数を作成する際のロックも保証します。 しかし、標準ではこれが保証されていません。 コードを変更し、
InjectPtr
キーパーをグローバルな静的変数に配置する必要がありました。これはおそらくプログラムコードが実行される前でも初期化されていました。 もちろん、別々の変数でできますが、それぞれの名前を作成する必要があります。 ここで、
CoreStorage
はコアIOCモジュールのキーパーです。
IOCModule.cpp
#include "Hoster.h" #include "SomeService.h" #include "InjectPtr.h" #include <IOCModule.h> #include <IOC.h> //Module like http://tapestry.apache.org/tapestry-ioc-modules.html //Now only for: - To provide explicit code for building a service using namespace ioc; struct CoreStorage { InjectPtr<SomeService> someService; InjectPtr<Hoster> hoster; }; static CoreStorage storage; ///methods like http://tapestry.apache.org/defining-tapestry-ioc-services.html#DefiningTapestryIOCServices-ServiceBuilderMethods template<> InjectPtr<SomeService> resolve<SomeService>() { if(!storage.someService.getFactory()) { storage.someService.setFactory(boost::bind(boost::factory<SomeService*>())); } return storage.someService; } ///Hoster takes SomeService in constructor template<> InjectPtr<Hoster> resolve<Hoster>() { if(!storage.hoster.getFactory()) { storage.hoster.setFactory(boost::bind(boost::factory<Hoster*>(), resolve<SomeService>())); } return storage.hoster; }
IOCモジュールヘッダーファイル
この項目は、IOCモジュール内のコンポーネントの接続性をわずかに増加させますが、モジュール間の相互作用により減少します。
IOCモジュールの相互作用のために、モジュール自体と同じ名前のIOCモジュールのインターフェイスヘッダーを作成すると便利です。 以下を含む必要があります。
- クラスインターフェイスのIOCレベルモジュールでのpublicの包含。
- IOCレベルの転送および単純な構造のモジュールでの完全な宣言。
- プリプロセッサ定義モジュールのIOCレベルでパブリック。
また、パブリックヘッダーをインポートして実行するプライベートモジュールヘッダーがあると便利です。
- プロジェクトのすべてのクラスの予備発表。
- IOCモジュールおよび単純な構造の内部転送の完全な宣言。
- IOCプリプロセッサ定義モジュールの内部。