(C++ : Design-pattern) Composite Pattern

Posted by : at

Category : Cpp   Design-pattern


Composite 패턴

  • 객체들이 트리 구조로 구성하여 부분과 전체를 나타내는 계층구조로 만들어진다.
  • 개별 객체와 복합 객체를 구별하지 않고 동일한 방법으로 다루게 된다.

설명만 봐선 뭔… ;; 차라리 코드를 보자

#include <iostream>
#include <string>
#include <vector>
using namespace std;

class BaseMenu
{
    string title;
public:
    BaseMenu(string s) : title(s) {}
    string getTitle() const { return title; }
    virtual void command() = 0;
};

class MenuItem : public BaseMenu
{
    int id;
public:
    MenuItem(string s, int n) : BaseMenu(s), id(n) {}       
    // BaseMenu에 디폴트 생성자가 없기에 BaseMenu(s)를 반드시 넣어 줘야함.
    virtual void command() override {
        cout << getTitle() << endl;
        getchar();
    }
};

class PopupMenu : public BaseMenu
{
    vector<BaseMenu*> v;
public:
    PopupMenu(string s) : BaseMenu(s) {}
    void addMenu(BaseMenu* p) { v.push_back(p); }

    virtual void command() override {
        while(1)
        {
            system("cls");
            int sz = v.size();
            for(int i = 0; i < sz; i++)
            {
                cout <<  i + 1 << ". " << v[i]->getTitle() << endl;
            }
            cout << sz + 1 << ". 상위 메뉴로" << endl;
            int cmd;
            cout << "메뉴를 선택하세요 >>";
            cin >> cmd;

            if(cmd<1 || cmd > sz+1) continue;       // 잘못된 입력
            if(cmd == sz+1) break;

            // 선택된 메뉴 실행
            v[cmd-1]->command();        
            // 핵심 -> 다형성으로 MenuItem, PopupMenu의 command를 모두 처리가능
        }
    }
};


int main()
{
    PopupMenu* menubar = new PopupMenu("MenuBar");
    PopupMenu* pm1 = new PopupMenu("화면설정");
    PopupMenu* pm2 = new PopupMenu("소리설정");

    MenuItem m1("정보 확인", 11);     


    menubar->addMenu(pm1);
    menubar->addMenu(pm2);
    menubar->addMenu(m1);

    pm1->addMenu(new MenuItem("해상도변경", 21);
    pm1->addMenu(new MenuItem("명암변경", 22);

    pm2->addMenu(new MenuItem("음량조절", 31);

    // 시작
    menubar->command();
}

아래와 같은 기능을 추가하고 싶다면?

int main()
{
    PopupMenu* menubar = new PopupMenu("MenuBar");
    PopupMenu* pm1 = new PopupMenu("화면설정");
    PopupMenu* pm2 = new PopupMenu("소리설정");

    MenuItem m1("정보 확인", 11);     


    menubar->addMenu(pm1);
    menubar->addMenu(pm2);
    menubar->addMenu(m1);

    pm1->addMenu(new MenuItem("해상도변경", 21);
    pm1->addMenu(new MenuItem("명암변경", 22);
    pm2->addMenu(new MenuItem("음량조절", 31);

    // 하위 메뉴의 포인터를 받고 싶다
    BaseMenu* p = menubar->getSubMenu(1)->getSubMenu(0);

    menubar->command();
}
class PopupMenu : public BaseMenu
{
    vector<BaseMenu*> v;
public:
    PopupMenu(string s) : BaseMenu(s) {}
    void addMenu(BaseMenu* p) { v.push_back(p); }
    BaseMenu* getSubMenu(int idx) { return v[idx]; }      
    // PopupMenu에만 넣으면 될까? -> Nope, BaseMenu에 넣어줘야함 SubMenu가 PopupMenu일지 MenuItem일지 모르기때문
class BaseMenu
{
    string title;
public:
    BaseMenu(string s) : title(s) {}
    string getTitle() const { return title; }
    virtual void command() = 0;

    virtual BaseMenu* getSubMenu(int idx)
    {
        throw "unsupported function";        
        // MenuItem에서 호출시 throw를 던저버린다.
        return 0;
    }
};

기능 추가) 각 메뉴에서 새로운 기능을 추가하고자 한다면?

#include "menu.hpp"

class MenuItem : public BaseMenu
{
    int id;
public:
    MenuItem(string s, int n) : BaseMenu(s), id(n) {}
    virtual void command()
    {
        // 여기는 실제 동작이 들어가는데 ...
        // 각 메뉴마다 다른 일을 수행해야한다. -> 어떻게 만들지??

        // 방법 1. - 가상 함수 처리
        doCommand();
    }
    virtual void doCommand() {}
};

class AddStudentMenu : public MenuItem
{
public:
    using MenuItem::MenuItem;   // 생성자 상속
    virtual void doCommand() { cout << "Add Student" << endl; }
};

int main()
{
    AddStudentMenu m1("Add Student ", 11);

    m1.command();
}

그런데 이런 방식은 메뉴가 50개라면 50개의 클래스를 만들어야한다.

struct IMenuListner
{
    virtual void doCommand(int id) = 0;
    virtual ~IMenuListner() {}
};

class MenuItem : public BaseMenu
{
    int id;
    IMenuListner* pListner = 0;
public:
    MenuItem(string s, int n) : BaseMenu(s), id(n) {}
    void setLisnter(IMenuListner* p) {pListner = p;}
    virtual void command()
    {
        // 방법 2. - 변하는 것을 다른 클래스로 뽑자.
        if(pListner != 0)
            pListner->doCommand(id);
    }
};

class Dialog : public IMenuListner
{
public:
    virtual void doCommand(int id)
    {
        switch(id)
        {
        case 11: cout << "Diglog doCommand : 11" << endl; break;
        case 12: cout << "Diglog doCommand : 12" << endl; break;
        defualt: break;
        }
        
    }
}

int main()
{
    Diglog dlg;
    MenuItem m1("Add Student", 11);

    m1.setLisnter(&dlg);

    m1.command();
}

위 방식은 switch문이 너무 커지게 될꺼 같은데??

그래서 나온게 function 템플릿!

우선, 일반/멤버 함수 포인터를 하나의 변수에 담고 싶다면

// 일단은 이 부분을 먼저 이해하고 들어가자
#include <iostream>
using namespace std;

void foo() { cout << "foo" << endl; }

class Dialog{
public:
    void Close() { cout << "Dialog Close" << endl; }
};

int main()
{
    // 이런게 하고싶다 void(*f1)()에는 &foo만 넣고 있고
    // void(Dialog::*f2)()에는 &Dialog::Close를 넣고있는데
    // 그냥 상관없이 &foo, &Dialog::Close 를 넣고 싶다면?

    // 정리하면 일반 함수 포인터(&foo)나 멤버 함수 포인터(&Dialog::Close)에 상관없이 모든 함수포인터를 넣고싶다는 말

    void(*f1)() = &foo;
    void(Dialog::*f2)() = &Dialog::Close;
}
struct IAction
{
    virtual void Execute() = 0;
    virtual ~IAction() {}
};

class FuctionAction : public IAction
{
    typedef void(*FP)();
    FP handler;
public:
    FuctionAction(FP f) : handler(f) {}
    virtual Execute() { handler(); }
};

template<typename T>
class MemberAction : public IAction
{
    typedef void(T::*FP)();
    FP handler;
public:
    MemberAction(FP f, T* obj) : handler(f), target(obj) {}
    virtual Execute() { (target->*handler)(); }
};

int main()
{
    Dialog dlg;
    IAction* p = new FunctionAction(&foo);
    IAction* p2 = new MemberAction<Dialog>(&Dialog::Close, &dlg);

    p->Execute();       // foo 실행
    p2->Execute();      // Close 실행
}

코드를 좀 더 간단히 해보자.

// 우선 아래를 이해해보자.
template<typename T> void square(T a) { return a * a; }

square<int>(3);
square(3);              // 3을 보고 컴파일러가 타입을 추론

list<int> s(10, 3);     // 클래스 템플릿은 타입추론이 안됨.

위 방법과 같이 타입추론을 컴파일러가 해주는데 new MemberAction<Dialog>(&Dialog::Close, &dlg);에서도 타입 추론이 안되나??

// 함수 템플릿 -> 타입 추론 가능
template<typename T>
MemberAction<T>* action(void(T::*f)(), T* obj)
{
    return new MemberAction<T>(f, obj);
}

int main()
{
    Dialog dlg;
    IAction* p = new FunctionAction(&foo);
    // IAction* p2 = new MemberAction<Dialog>(&Dialog::Close, &dlg);
    IAction* p2 = action(&Dialog::Close, &dlg);     // ok

    p->Execute();       // foo 실행
    p2->Execute();      // Close 실행
}

코드에 일관성을 부여해보자.

// 함수 템플릿 -> 타입 추론 가능
template<typename T>
MemberAction<T>* action(void(T::*f)(), T* obj)
{
    return new MemberAction<T>(f, obj);
}

FunctionAction* action( void(*f)() )
{
    return new FunctionAction(f);
}

int main()
{
    Dialog dlg;
    // IAction* p = new FunctionAction(&foo);
    IAction* p = action(&foo);
    // IAction* p2 = new MemberAction<Dialog>(&Dialog::Close, &dlg);
    IAction* p2 = action(&Dialog::Close, &dlg);     // ok

    p->Execute();       // foo 실행
    p2->Execute();      // Close 실행
}

이런 기능을 C++ 자체에서 제공한다.

function 템플릿

C++11 부터 지원되는 일반화된 함수 포인터 역할 템플릿

#include <iostream>
#include <functional>
using namespace std;

void foo() { cout << "foo" << endl; }
void goo(int) { cout << "goo" << endl; }

class Dialog
{
public:
    void Close() { cout << "Dialog Close" << endl; }
};

int main()
{
    function<void()> f;
    f = &foo;
    f();        // foo 호출

    Dialog dlg;

    f = bind(&Dialog::Close, &dlg);
    f();        // dlg.Close() 호출

    f = bind(&goo, 5);
    f();        // goo(5) 호출
}

About Taehyung Kim

안녕하세요? 8년차 현업 C++ 개발자 김태형이라고 합니다. 😁 C/C++을 사랑하며 다양한 사람과의 협업을 즐깁니다. ☕ 꾸준한 자기개발을 미덕이라 생각하며 노력중이며, 제가 얻은 지식을 홈페이지에 정리 중입니다. 좀 더 상세한 제 이력서 혹은 Private 프로젝트 접근 권한을 원하신다면 메일주세요. 😎

Star
Useful Links