수색…


소개

이 페이지에서는 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

다음은 패턴 요약입니다.

  1. 객체 ( DigitalClock 또는 AnalogClock 객체)는 관찰자로 등록 (등록)하거나 관찰자 ( subject.Attach(*this); subject.Detach(*this); subject.Attach(*this); 로부터 자신을 구독 취소 (제거)하기 위해 제목 인터페이스 ( Attach() 또는 Detach() subject.Detach(*this);

  2. 각 주제에는 많은 옵저버 ( vector<Observer*> observers; )가있을 수 있습니다.

  3. 모든 옵저버는 Observer 인터페이스를 구현해야합니다. 이 인터페이스에는 Subject의 상태가 변경 될 때 호출되는 Update() 메서드가 하나 있습니다 ( Update(Subject &) ).

  4. Attach()Detach() 메서드 외에도 구체적인 주체는 상태가 변경 될 때마다 모든 현재 관찰자를 업데이트하는 데 사용되는 Notify() 메서드를 구현합니다. 그러나이 경우 모두 부모 클래스 인 Subject ( Subject::Attach (Observer&) , void Subject::Detach(Observer&)void Subject::Notify() 됩니다.

  5. Concrete 객체는 상태를 설정하고 가져 오는 메소드를 가질 수도 있습니다.

  6. 구체적인 관찰자는 Observer 인터페이스를 구현하는 모든 클래스가 될 수 있습니다. 각 관찰자는 업데이트를 수신 할 구체적인 주제 ( subject.Attach(*this); )를 구독 (등록)합니다.

  7. 옵저버 패턴의 두 객체는 느슨하게 결합되어 상호 작용할 수 있지만 서로에 대해 거의 알지 못합니다.

변화:

신호 및 슬롯

신호와 슬롯은 Qt에 도입 된 언어 구조로, 상용구 코드를 피하면서 Observer 패턴을 쉽게 구현할 수 있습니다. 개념은 컨트롤 (위젯이라고도 함)이 슬롯이라고하는 특수 기능을 사용하여 다른 컨트롤에서받을 수있는 이벤트 정보를 포함하는 신호를 보낼 수 있다는 것입니다. Qt의 슬롯은 반드시 선언 된 클래스 멤버 여야합니다. 신호 / 슬롯 시스템은 그래픽 사용자 인터페이스가 설계된 방식에 잘 맞습니다. 비슷하게 신호 / 슬롯 시스템은 비동기 I / O (소켓, 파이프, 직렬 장치 등을 포함하여) 이벤트 통지 또는 타임 아웃 이벤트를 적절한 객체 인스턴스 및 메소드 또는 함수와 연관시키는 데 사용될 수 있습니다. Qt의 Meta Object Compiler (MOC)는 자동으로 필요한 인프라를 생성하기 때문에 등록 / 등록 해제 / 호출 코드를 작성할 필요가 없습니다.

C # 언어는 다른 용어와 구문을 사용하여 유사한 구조를 지원합니다. 이벤트는 신호의 역할을 담당하고 대리자는 슬롯입니다. 또한 델리게이트는 함수 포인터와 마찬가지로 로컬 변수가 될 수 있지만 Qt의 슬롯은 선언 된 클래스 멤버 여야합니다.

어댑터 패턴

클래스의 인터페이스를 클라이언트가 기대하는 다른 인터페이스로 변환하십시오. Adapter (또는 Wrapper)를 사용하면 호환되지 않는 인터페이스로 인해 클래스를 함께 사용할 수 있습니다. 어댑터 패턴의 동기는 인터페이스를 수정할 수 있다면 기존 소프트웨어를 재사용 할 수 있다는 것입니다.

  1. 어댑터 패턴은 오브젝트 구성에 의존합니다.

  2. 클라이언트는 Adapter 객체에서 작업을 호출합니다.

  3. 어댑터는 Adaptee를 호출하여 작업을 수행합니다.

  4. 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)

코드 요약 :

  1. 클라이언트는 자신이 Rectangle 대화하고 있다고 생각합니다.

  2. 대상은 Rectangle 클래스입니다. 이것은 클라이언트가 method를 호출하는 것입니다.

     Rectangle *r = new RectangleAdapter(x,y,w,h);
     r->draw();
    
  3. 어댑터 클래스는 다중 상속을 사용합니다.

     class RectangleAdapter: public Rectangle, private LegacyRectangle {
         ...
     }
    
  4. Adapter RectangleAdapter 사용하면 LegacyRectangle 클래스를 모두 상속하여 요청 ( Rectangle draw() 에 응답) 할 수 있습니다.

  5. 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 객체는 변경 불가능합니다. 즉, 속성에 대한 액세스가 없기 때문에 속성을 수정할 수 없습니다. 이것은 원하는 기능이었습니다. 객체를 만든 후에 객체를 수정해야하는 경우 객체에 대한 세터를 제공해야합니다. 이러한 세터는 빌더에서 복제되므로 하나의 클래스에서 모든 빌더 클래스를 더 이상 고려하지 않아도됩니다. 그럼에도 불구하고, 나는 처음부터 빌드 된 객체를 변경 가능하게 만들 필요성을 고려할 것이다.



Modified text is an extract of the original Stack Overflow Documentation
아래 라이선스 CC BY-SA 3.0
와 제휴하지 않음 Stack Overflow