C++
C ++의 디자인 패턴 구현
수색…
소개
이 페이지에서는 C ++에서 디자인 패턴이 어떻게 구현되는지 예제를 볼 수 있습니다. 이러한 패턴에 대한 자세한 내용 은 디자인 패턴 문서를 참조하십시오 .
비고
디자인 패턴은 소프트웨어 디자인의 특정 상황에서 일반적으로 발생하는 문제에 대한 일반적인 재사용 가능한 솔루션입니다.
옵저버 패턴
Observer Pattern의 목적은 객체간에 일대 다 의존성을 정의하여 하나의 객체가 상태를 변경할 때 모든 종속 객체가 자동으로 통지되고 업데이트되도록하는 것입니다.
주체와 관찰자는 일대 다 관계를 정의합니다. 관찰자는 주체의 상태가 변경되면 관찰자에게 통보되도록 대상에 종속됩니다. 통보에 따라 옵저버는 새로운 값으로 업데이트 될 수도 있습니다.
다음은 감마 (Gamma)의 "디자인 패턴 (Design Patterns)"책의 예입니다.
#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
객체)는 관찰자로 등록 (등록)하거나 관찰자 (subject.Attach(*this);
subject.Detach(*this);
subject.Attach(*this);
로부터 자신을 구독 취소 (제거)하기 위해 제목 인터페이스 (Attach()
또는Detach()
subject.Detach(*this);
각 주제에는 많은 옵저버 (
vector<Observer*> observers;
)가있을 수 있습니다.모든 옵저버는 Observer 인터페이스를 구현해야합니다. 이 인터페이스에는 Subject의 상태가 변경 될 때 호출되는
Update()
메서드가 하나 있습니다 (Update(Subject &)
).Attach()
및Detach()
메서드 외에도 구체적인 주체는 상태가 변경 될 때마다 모든 현재 관찰자를 업데이트하는 데 사용되는Notify()
메서드를 구현합니다. 그러나이 경우 모두 부모 클래스 인Subject
(Subject::Attach (Observer&)
,void Subject::Detach(Observer&)
및void Subject::Notify()
됩니다.Concrete 객체는 상태를 설정하고 가져 오는 메소드를 가질 수도 있습니다.
구체적인 관찰자는 Observer 인터페이스를 구현하는 모든 클래스가 될 수 있습니다. 각 관찰자는 업데이트를 수신 할 구체적인 주제 (
subject.Attach(*this);
)를 구독 (등록)합니다.옵저버 패턴의 두 객체는 느슨하게 결합되어 상호 작용할 수 있지만 서로에 대해 거의 알지 못합니다.
변화:
신호 및 슬롯
신호와 슬롯은 Qt에 도입 된 언어 구조로, 상용구 코드를 피하면서 Observer 패턴을 쉽게 구현할 수 있습니다. 개념은 컨트롤 (위젯이라고도 함)이 슬롯이라고하는 특수 기능을 사용하여 다른 컨트롤에서받을 수있는 이벤트 정보를 포함하는 신호를 보낼 수 있다는 것입니다. Qt의 슬롯은 반드시 선언 된 클래스 멤버 여야합니다. 신호 / 슬롯 시스템은 그래픽 사용자 인터페이스가 설계된 방식에 잘 맞습니다. 비슷하게 신호 / 슬롯 시스템은 비동기 I / O (소켓, 파이프, 직렬 장치 등을 포함하여) 이벤트 통지 또는 타임 아웃 이벤트를 적절한 객체 인스턴스 및 메소드 또는 함수와 연관시키는 데 사용될 수 있습니다. Qt의 Meta Object Compiler (MOC)는 자동으로 필요한 인프라를 생성하기 때문에 등록 / 등록 해제 / 호출 코드를 작성할 필요가 없습니다.
C # 언어는 다른 용어와 구문을 사용하여 유사한 구조를 지원합니다. 이벤트는 신호의 역할을 담당하고 대리자는 슬롯입니다. 또한 델리게이트는 함수 포인터와 마찬가지로 로컬 변수가 될 수 있지만 Qt의 슬롯은 선언 된 클래스 멤버 여야합니다.
어댑터 패턴
클래스의 인터페이스를 클라이언트가 기대하는 다른 인터페이스로 변환하십시오. Adapter (또는 Wrapper)를 사용하면 호환되지 않는 인터페이스로 인해 클래스를 함께 사용할 수 있습니다. 어댑터 패턴의 동기는 인터페이스를 수정할 수 있다면 기존 소프트웨어를 재사용 할 수 있다는 것입니다.
어댑터 패턴은 오브젝트 구성에 의존합니다.
클라이언트는 Adapter 객체에서 작업을 호출합니다.
어댑터는 Adaptee를 호출하여 작업을 수행합니다.
STL에서 벡터로부터 적응 된 스택 : 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
클래스입니다. 이것은 클라이언트가 method를 호출하는 것입니다.Rectangle *r = new RectangleAdapter(x,y,w,h); r->draw();
어댑터 클래스는 다중 상속을 사용합니다.
class RectangleAdapter: public Rectangle, private LegacyRectangle { ... }
Adapter
RectangleAdapter
사용하면LegacyRectangle
클래스를 모두 상속하여 요청 (Rectangle
draw()
에 응답) 할 수 있습니다.LegacyRectangle
클래스는 같은 방법 (없는draw()
등)Rectangle
하지만,Adapter(RectangleAdapter)
테이크 수Rectangle
메소드 호출을하고 돌아서과에 메소드를 호출LegacyRectangle
,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를 제공 할 수 있습니다.
브리지 패턴 은 객체 어댑터와 비슷한 구조를 가지고 있지만 Bridge는 다른 의도를 가지고 있습니다. 인터페이스를 구현과 분리 하여 쉽고 독립적으로 변경할 수 있습니다. 어댑터 는 기존 개체 의 인터페이스 를 변경하기 위한 것입니다.
공장 패턴
팩토리 패턴은 객체 생성을 분리하고 공통 인터페이스를 사용하여 이름으로 생성을 허용합니다.
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;
}
};
유창한 API가있는 작성기 패턴
빌더 패턴은 오브젝트 작성과 오브젝트 자체를 분리합니다. 뒤에있는 주된 아이디어 는 객체가 자신의 생성에 대한 책임을 질 필요가 없다는 것 입니다. 복잡한 객체의 정확하고 유효한 어셈블리는 그 자체로 복잡한 작업 일 수 있으므로이 작업을 다른 클래스에 위임 할 수 있습니다.
C # 의 이메일 작성기에서 영감을 얻어 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&&()
의해 operator Email&&()
전자 메일을 릴리스 할 때 작업을 완료합니다. 이 예제에서 빌더는 임시 객체이며 파괴되기 전에 이메일을 반환합니다. 변환 연산자 대신 Email EmailBuilder::build() {...}
와 같은 명시적인 작업을 사용할 수도 있습니다.
빌더를 지나쳐라.
Builder Pattern이 제공하는 가장 큰 특징은 여러 액터를 사용하여 객체를 함께 구성 할 수 있다는 것입니다. 이것은 빌더를 다른 액터로 전달하여 각각의 액터가 빌드 된 오브젝트에 더 많은 정보를 제공하도록합니다. 이것은 일종의 쿼리를 작성하고 필터 및 기타 사양을 추가 할 때 특히 강력합니다.
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;
}
디자인 변형 : 가변 객체
이 패턴의 디자인을 필요에 맞게 변경할 수 있습니다. 하나의 변종을 드리겠습니다.
주어진 예제에서 Email 객체는 변경 불가능합니다. 즉, 속성에 대한 액세스가 없기 때문에 속성을 수정할 수 없습니다. 이것은 원하는 기능이었습니다. 객체를 만든 후에 객체를 수정해야하는 경우 객체에 대한 세터를 제공해야합니다. 이러한 세터는 빌더에서 복제되므로 하나의 클래스에서 모든 빌더 클래스를 더 이상 고려하지 않아도됩니다. 그럼에도 불구하고, 나는 처음부터 빌드 된 객체를 변경 가능하게 만들 필요성을 고려할 것이다.