C++
C ++でのデザインパターンの実装
サーチ…
前書き
このページでは、デザインパターンがC ++でどのように実装されているかの例を見つけることができます。これらのパターンの詳細については、デザインパターンのドキュメントを参照してください。
備考
デザインパターンは、ソフトウェア設計の特定のコンテキスト内で一般的に発生する問題に対する一般的な再利用可能なソリューションです。
オブザーバーパターン
Observer Patternの目的は、オブジェクト間で1対多の依存関係を定義して、1つのオブジェクトが状態を変更したときにそのすべての従属オブジェクトが自動的に通知および更新されるようにすることです。
被験者とオブザーバーは、一対多の関係を定義します。オブザーバーは、被験者の状態が変化するとオブザーバーに通知されるように、被験者に依存しています。通知に応じて、オブザーバーは新しい値で更新されることもあります。
ここでは、ガンマによる本の「デザインパターン」の例を示します。
#include <iostream>
#include <vector>
class Subject;
class Observer
{
public:
virtual ~Observer() = default;
virtual void Update(Subject&) = 0;
};
class Subject
{
public:
virtual ~Subject() = default;
void Attach(Observer& o) { observers.push_back(&o); }
void Detach(Observer& o)
{
observers.erase(std::remove(observers.begin(), observers.end(), &o));
}
void Notify()
{
for (auto* o : observers) {
o->Update(*this);
}
}
private:
std::vector<Observer*> observers;
};
class ClockTimer : public Subject
{
public:
void SetTime(int hour, int minute, int second)
{
this->hour = hour;
this->minute = minute;
this->second = second;
Notify();
}
int GetHour() const { return hour; }
int GetMinute() const { return minute; }
int GetSecond() const { return second; }
private:
int hour;
int minute;
int second;
};
class DigitalClock: public Observer
{
public:
explicit DigitalClock(ClockTimer& s) : subject(s) { subject.Attach(*this); }
~DigitalClock() { subject.Detach(*this); }
void Update(Subject& theChangedSubject) override
{
if (&theChangedSubject == &subject) {
Draw();
}
}
void Draw()
{
int hour = subject.GetHour();
int minute = subject.GetMinute();
int second = subject.GetSecond();
std::cout << "Digital time is " << hour << ":"
<< minute << ":"
<< second << std::endl;
}
private:
ClockTimer& subject;
};
class AnalogClock: public Observer
{
public:
explicit AnalogClock(ClockTimer& s) : subject(s) { subject.Attach(*this); }
~AnalogClock() { subject.Detach(*this); }
void Update(Subject& theChangedSubject) override
{
if (&theChangedSubject == &subject) {
Draw();
}
}
void Draw()
{
int hour = subject.GetHour();
int minute = subject.GetMinute();
int second = subject.GetSecond();
std::cout << "Analog time is " << hour << ":"
<< minute << ":"
<< second << std::endl;
}
private:
ClockTimer& subject;
};
int main()
{
ClockTimer timer;
DigitalClock digitalClock(timer);
AnalogClock analogClock(timer);
timer.SetTime(14, 41, 36);
}
出力:
Digital time is 14:41:36
Analog time is 14:41:36
パターンの要約は次のとおりです。
オブジェクト(
DigitalClock
またはAnalogClock
オブジェクト)テーマインタフェースを使用(Attach()
またはDetach()
のいずれかを購読(登録)オブザーバーとして、または解除(削除)自身である観察者から(するsubject.Attach(*this);
、subject.Detach(*this);
各被験者は多くのオブザーバー(
vector<Observer*> observers;
)を持つことができます。すべてのオブザーバはObserverインタフェースを実装する必要があります。このインタフェースには、件名の状態が変更されたときに呼び出される1つのメソッド
Update()
があります(Update(Subject &)
)Attach()
メソッドとDetach()
メソッドに加えて、具体的なサブジェクトは、状態が変わるたびに現在のオブザーバをすべて更新するためのNotify()
メソッドを実装しています。しかし、この場合、それらのすべては、親クラスSubject
(Subject::Attach (Observer&)
、void Subject::Detach(Observer&)
およびvoid Subject::Notify()
ます。Concreteオブジェクトには、その状態を設定し取得するメソッドもあります。
具体的なオブザーバは、Observerインターフェイスを実装する任意のクラスです。各オブザーバーは、更新を受信する具体的なサブジェクト(
subject.Attach(*this);
)をサブスクライブ(登録)しsubject.Attach(*this);
。Observer Patternの2つのオブジェクトは緩やかに結合されていますが、相互作用はできますが、お互いの知識はほとんどありません。
変化:
信号とスロット
Signals and slotsはQtで導入された言語構造であり、定型コードを避けながらObserverパターンの実装を容易にします。概念は、コントロール(ウィジェットとしても知られている)は、スロットと呼ばれる特別な機能を使用して他のコントロールで受信できるイベント情報を含む信号を送信できるということです。 Qtのスロットは、そのように宣言されたクラスメンバでなければなりません。信号/スロットシステムは、グラフィカルユーザインタフェースの設計方法に適しています。同様に、シグナル/スロットシステムは、非同期入出力(ソケット、パイプ、シリアルデバイスなどを含む)イベント通知、またはタイムアウトイベントを適切なオブジェクトインスタンスおよびメソッドまたは関数に関連付けるために使用できます。 QtのMeta Object Compiler(MOC)は必要なインフラストラクチャを自動的に生成するため、登録/登録解除/呼び出しコードを記述する必要はありません。
C#言語でも、同様の構文がサポートされていますが、異なる用語と構文が使用されています。イベントはシグナルの役割を果たし、代理人はスロットです。さらに、デリゲートは関数ポインタのようにローカル変数になりますが、Qtのスロットはそのように宣言されたクラスメンバでなければなりません。
アダプタパターン
クラスのインターフェイスを、クライアントが期待する別のインターフェイスに変換します。 Adapter(またはWrapper)を使用すると、互換性のないインタフェースが原因でクラスが連携できなくなります。アダプタパターンの動機付けは、インタフェースを変更できる場合に既存のソフトウェアを再利用できるということです。
アダプタパターンは、オブジェクトの構成に依存します。
クライアントはAdapterオブジェクトに対して操作を呼び出します。
アダプタはAdapteeを呼び出して操作を実行します。
STLでは、vectorから適合するスタック:stackがpush()を実行すると、下にあるベクトルはvector :: push_back()を実行します。
例:
#include <iostream>
// Desired interface (Target)
class Rectangle
{
public:
virtual void draw() = 0;
};
// Legacy component (Adaptee)
class LegacyRectangle
{
public:
LegacyRectangle(int x1, int y1, int x2, int y2) {
x1_ = x1;
y1_ = y1;
x2_ = x2;
y2_ = y2;
std::cout << "LegacyRectangle(x1,y1,x2,y2)\n";
}
void oldDraw() {
std::cout << "LegacyRectangle: oldDraw(). \n";
}
private:
int x1_;
int y1_;
int x2_;
int y2_;
};
// Adapter wrapper
class RectangleAdapter: public Rectangle, private LegacyRectangle
{
public:
RectangleAdapter(int x, int y, int w, int h):
LegacyRectangle(x, y, x + w, y + h) {
std::cout << "RectangleAdapter(x,y,x+w,x+h)\n";
}
void draw() {
std::cout << "RectangleAdapter: draw().\n";
oldDraw();
}
};
int main()
{
int x = 20, y = 50, w = 300, h = 200;
Rectangle *r = new RectangleAdapter(x,y,w,h);
r->draw();
}
//Output:
//LegacyRectangle(x1,y1,x2,y2)
//RectangleAdapter(x,y,x+w,x+h)
コードの要約:
クライアントは、彼が
Rectangle
と話していると考えているターゲットは
Rectangle
クラスです。これは、クライアントがメソッドを呼び出すためのものです。Rectangle *r = new RectangleAdapter(x,y,w,h); r->draw();
アダプタクラスは多重継承を使用することに注意してください。
class RectangleAdapter: public Rectangle, private LegacyRectangle { ... }
Adapter
RectangleAdapter
使用すると、LegacyRectangle
は、両方のクラスを継承して、要求(Rectangle
上のdraw()
に応答します)。LegacyRectangle
クラスはRectangle
と同じメソッド(draw()
)を持ちませんが、Adapter(RectangleAdapter)
はRectangle
メソッド呼び出しをLegacyRectangle
、oldDraw()
、oldDraw()
メソッドを呼び出して呼び出します。class RectangleAdapter: public Rectangle, private LegacyRectangle { public: RectangleAdapter(int x, int y, int w, int h): LegacyRectangle(x, y, x + w, y + h) { std::cout << "RectangleAdapter(x,y,x+w,x+h)\n"; } void draw() { std::cout << "RectangleAdapter: draw().\n"; oldDraw(); } };
アダプタ設計パターンは、あるクラスのインタフェースを互換性のある異なるインタフェースに変換します。したがって、これは単一コンポーネントラッパーである点でプロキシパターンに似ています。しかし、アダプタクラスと元のクラスのインタフェースは異なる場合があります。
上の例で見たように、このアダプターパターンは、既存のAPIに対して別のインターフェースを公開して他のコードとの連携を可能にするのに便利です。また、アダプターパターンを使用することで、異機種インターフェースを使用して、一貫したAPIを提供するように変換することができます。
ブリッジ・パターンはオブジェクト・アダプターと似た構造ですが、ブリッジは異なる意図を持っています。インタフェースを実装から分離して 、容易に独立して変更できるようにします。 アダプターは、 既存のオブジェクトのインターフェースを変更するためのものです。
工場パターン
ファクトリパターンはオブジェクト作成を切り離し、共通インタフェースを使用して名前で作成することができます。
class Animal{
public:
virtual std::shared_ptr<Animal> clone() const = 0;
virtual std::string getname() const = 0;
};
class Bear: public Animal{
public:
virtual std::shared_ptr<Animal> clone() const override
{
return std::make_shared<Bear>(*this);
}
virtual std::string getname() const override
{
return "bear";
}
};
class Cat: public Animal{
public:
virtual std::shared_ptr<Animal> clone() const override
{
return std::make_shared<Cat>(*this);
}
virtual std::string getname() const override
{
return "cat";
}
};
class AnimalFactory{
public:
static std::shared_ptr<Animal> getAnimal( const std::string& name )
{
if ( name == "bear" )
return std::make_shared<Bear>();
if ( name == "cat" )
return std::shared_ptr<Cat>();
return nullptr;
}
};
Fluent APIを使用したBuilderパターン
Builderパターンは、オブジェクトの作成をオブジェクト自体から切り離します。主なアイデアは、オブジェクトが自身の作成に責任を負う必要はないということです。複雑なオブジェクトの正しいアセンブリと有効なアセンブリは、複雑な作業である可能性があるため、このタスクを別のクラスに委任することができます。
C#のEmail Builderに触発されて、私はここでC ++バージョンを作ることに決めました。 Emailオブジェクトは、必ずしも非常に複雑なオブジェクトではありませんが、パターンを示すことができます。
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
// Forward declaring the builder
class EmailBuilder;
class Email
{
public:
friend class EmailBuilder; // the builder can access Email's privates
static EmailBuilder make();
string to_string() const {
stringstream stream;
stream << "from: " << m_from
<< "\nto: " << m_to
<< "\nsubject: " << m_subject
<< "\nbody: " << m_body;
return stream.str();
}
private:
Email() = default; // restrict construction to builder
string m_from;
string m_to;
string m_subject;
string m_body;
};
class EmailBuilder
{
public:
EmailBuilder& from(const string &from) {
m_email.m_from = from;
return *this;
}
EmailBuilder& to(const string &to) {
m_email.m_to = to;
return *this;
}
EmailBuilder& subject(const string &subject) {
m_email.m_subject = subject;
return *this;
}
EmailBuilder& body(const string &body) {
m_email.m_body = body;
return *this;
}
operator Email&&() {
return std::move(m_email); // notice the move
}
private:
Email m_email;
};
EmailBuilder Email::make()
{
return EmailBuilder();
}
// Bonus example!
std::ostream& operator <<(std::ostream& stream, const Email& email)
{
stream << email.to_string();
return stream;
}
int main()
{
Email mail = Email::make().from("[email protected]")
.to("[email protected]")
.subject("C++ builders")
.body("I like this API, don't you?");
cout << mail << endl;
}
古いバージョンのC ++では、 std::move
オペレーションを無視し、変換オペレータから&&を削除することができます(ただし、一時コピーが作成されます)。
ビルダーは、 operator Email&&()
によってビルドされた電子メールを解放すると、作業を終了します。この例では、ビルダは一時オブジェクトであり、破棄される前に電子メールを返します。また、変換演算子の代わりにEmail EmailBuilder::build() {...}
ような明示的な操作を使用することもできます。
ビルダーを周りに渡す
Builderパターンが提供する素晴らしい機能は、 複数のアクタを使用してオブジェクトを一緒に構築する機能です。これは、ビルダを他のアクタに渡すことによって行われます。これらのアクタは、それぞれがビルドされたオブジェクトにさらに情報を与えます。これは、何らかのクエリを作成してフィルタやその他の仕様を追加する場合に特に効果的です。
void add_addresses(EmailBuilder& builder)
{
builder.from("[email protected]")
.to("[email protected]");
}
void compose_mail(EmailBuilder& builder)
{
builder.subject("I know the subject")
.body("And the body. Someone else knows the addresses.");
}
int main()
{
EmailBuilder builder;
add_addresses(builder);
compose_mail(builder);
Email mail = builder;
cout << mail << endl;
}
デザインの変更:可変オブジェクト
このパターンのデザインをニーズに合わせて変更することができます。私は1つの変形を与えるでしょう。
与えられた例では、Emailオブジェクトは不変です。つまり、そのオブジェクトへのアクセスがないため、プロパティを変更することはできません。これは望ましい機能でした。オブジェクトの作成後にオブジェクトを変更する必要がある場合は、そのオブジェクトにいくつかのセッターを提供する必要があります。これらのセッターはビルダーに複製されるので、1つのクラスですべての作業を行うことを検討することができます(ビルダークラスはもう必要ありません)。それにもかかわらず、私は、最初に組み込みオブジェクトを変更可能にする必要性を考慮する。