I'm FanJae.

[C++ Design Pattern] Strategy Pattern 본문

C++/Design Pattern

[C++ Design Pattern] Strategy Pattern

FanJae 2024. 9. 22. 20:34

1. Strategy Pattern의 정의

다양한 알고리즘이 존재 하면 이들 각각을 하나의 "클래스로 캡슐화하여 알고리즘의 대체가 가능"하도록 한다. Strategy 패턴을 이용하면 클라이언트와 독립적인 다양한 알고리즘으로 변형할 수 있다. 알고리즘을 바꾸더라도 클라이언트는 아무런 변경을 할 필요가 없다. 

 

1-1. 예제를 통한 Strategy Pattern의 필요성 이해

#include <iostream>
#include <string>
#include <conio.h>

class Edit
{
    std::string data;
public:
    std::string get_text()
    {
       std::cin >> data;
       return data;
    }
};

int main()
{
    Edit edit;
    while (1)
    {
         std::string s = edit.get_text();
         std::cout << s << std::endl;
    }
}

- Edit 클래스는 사용자에게 입력을 받을때 사용하는 GUI Widget으로 가정해보자. 

- 만약 Edit을 사용해서 나이를 입력받고 싶다고 가정하자.

※ 이 경우, 숫자만 입력되도록 제한(validation)해야 한다.

 

#include <iostream>
#include <string>
#include <conio.h>

class Edit
{
    std::string data;
public:
    std::string get_text()
    {
        data.clear();
        
        while(1)
        {
            char c = _getch();
            
            if (c == 13) break; // enter 키
            
            if (isdigit(c))
            {
                data.push_back(c);
                std::cout << c;
            }
        }
        std::cout << "\n";
        return data;
    }
};

int main()
{
    Edit edit;
    while (1)
    {
        std::string s = edit.get_text();
        std::cout << s << std::endl;
    }
}

- 이와 같이 구현 가능하다. 이번에는 만약 주소를 입력받고 싶었다고 가정해보자.

- 그러면, Edit의 Validation 정책은 변경될 수 있어야 한다.

- Edit과 같은 Class는 라이브러리 내부이다. 내부 코드를 원할때마다 바꾸는건 어려운 일이다.

- Edit을 만드는 사람은 내부 코드를 수정하지 않고도 다른 방법으로 Validation 정책을 바꿀 수 있도록 해야 한다.

 

1-2. Validation 정책 변경 방법

- 변하는 것을 분리할 때 사용하는 2가지 방법이 존재한다.

- 변하는 코드를 가상함수로 분리하는 것과 변하는 코드를 다른 클래스로 분리하는 방법이다.

#include <iostream>
#include <string>
#include <conio.h>

class Edit
{
    std::string data;
public:
    std::string get_text()
    {
        data.clear();
        
        while(1)
        {
            char c = _getch();
            
            if (c == 13) break; // enter 키
            
            if (isdigit(c))
            {
                data.push_back(c);
                std::cout << c;
            }
        }
        std::cout << "\n";
        return data;
    }
};

int main()
{
    Edit edit;
    while (1)
    {
        std::string s = edit.get_text();
        std::cout << s << std::endl;
    }
}

 

① 변하는 것을 가상함수로 분리 (template method 패턴)

#include <iostream>
#include <string>
#include <conio.h>

class Edit
{
    std::string data;
public:
    std::string get_text()
    {
        data.clear();

        while (1)
        {
            char c = _getch();

            if (c == 13 && iscomplete(data) ) break;

            if (validate(data,c))
            {
                data.push_back(c);
                std::cout << c;
            }

        }
        std::cout << "\n";
        return data;
    }
    virtual bool validate(const std::string& data, char c)
    {
        return true;
    }
    virtual bool iscomplete(const std::string& data)
    {
        return true;
    }
};

int main()
{
    Edit edit;
    while (1)
    {
        std::string s = edit.get_text();
        std::cout << s << std::endl;
    }
}

- iscomplete(data) 를 통해서 입력 값이 완성되었는지도 체크한다.

 

① 파생 클래스 적용

#include <iostream>
#include <string>
#include <conio.h>

class Edit
{
    std::string data;
public:
    std::string get_text()
    {
        data.clear();

        while (1)
        {
            char c = _getch();

            if (c == 13 && iscomplete(data) ) break;

            if (validate(data,c))
            {
                data.push_back(c);
                std::cout << c;
            }

        }
        std::cout << "\n";
        return data;
    }
    virtual bool validate(const std::string& data, char c)
    {
        return true;
    }
    virtual bool iscomplete(const std::string& data)
    {
        return true;
    }
};

class NumEdit : public Edit
{
    int count;
 public:
    NumEdit(int count = 9999) : count(count) {}
    bool validate(const std::string& data, char c) override
    {
         return data.size() < count && isdigit(c);
    }
    bool iscomplete(const std::string& data) override
    {
        return count != 9999 && data.size() == count; 
    }
};


int main()
{
    NumEdit edit(5); // 5자리 숫자만, 5자리 입력 되어야만 enter 가능
    while (1)
    {
        std::string s = edit.get_text();
        std::cout << s << std::endl;
    }
}

- template method가 과연 좋은 방법일까..?

 

② 다른 클래스로 분리하는 방법

- 인터페이스를 먼저 만들고 Edit에서 약한 결합으로 다양한 Validation 정책 클래스 사용

#include <iostream>
#include <string>
#include <conio.h>

struct IValidator
{
    virtual bool validate(const std::string& data, char c) = 0;
    virtual bool iscomplete(const std::string& data) { return true; }
    virtual ~IValidator() { }
};

class Edit
{
    std::string data;
    IValidator* val = nullptr;
public:
    void set_vailidator(IValidator* p) { val = p; }
    
    std::string get_text()
    {
        data.clear();
        
        while(1)
        {
            char c = _getch();
            
            if (c == 13 && (val == nullptr || val->iscomplete(data) ) ) break; // enter 키
            
            if (val == nullptr || val->validate(data,c))
            {
                data.push_back(c);
                std::cout << c;
            }
        }
        std::cout << "\n";
        return data;
    }
};
int main()
{
    Edit edit;
    while (1)
    {
        std::string s = edit.get_text();
        std::cout << s << std::endl;
    }
}

- Edit이 값의 유효성 여부를 체크하기 위해서 다른 클래스에 의존하고 있음을 알 수 있다.

- 이를 보통 위임한다고 한다.

 

1-2-1. Validator 구현

#include <iostream>
#include <string>
#include <conio.h>

struct IValidator
{
    virtual bool validate(const std::string& data, char c) = 0;
    virtual bool iscomplete(const std::string& data) { return true; }
    virtual ~IValidator() { }
};

class DigitValidator : public IValidator
{
   int count;
public:
   DigitValidator(int count = 9999) : count(count) { }
   
   bool validate(const std::string& data, char c) override
   {
        return data.size() < count && isdigit(c);
   }
   
   bool iscomplete(const std::string& data) override
   {
        return != 9999 && data.size() == count;
   }
};


class Edit
{
    std::string data;
    IValidator* val = nullptr;
public:
    void set_validator(IValidator* p) { val = p; }
    
    std::string get_text()
    {
        data.clear();
        
        while(1)
        {
            char c = _getch();
            
            if (c == 13 && (val == nullptr || val->iscomplete(data) ) ) break; // enter 키
            
            if (val == nullptr || val->validate(data,c))
            {
                data.push_back(c);
                std::cout << c;
            }
        }
        std::cout << "\n";
        return data;
    }
};
int main()
{
    Edit edit;
    DigitValidator v(5);
    edit.set_validator(&v);
    
    while (1)
    {
        std::string s = edit.get_text();
        std::cout << s << std::endl;
    }
}

- 다음과 같이 다른 클래스로 구현해서 사용할 수 있다.


2. Strategy(전략 패턴) 정리

① Validation 정책을 가상함수로 분리

- NumEdit이 Validation 정책을 소유하게 된다.

- 다른 클래스에서 Validation 정책을 사용할 수 없다.

- 실행시간에 Validation 정책을 교체할 수 없다.

 

② Validation 정책을 다른 클래스로 분리

- Edit와 Validation 정책이 서로 다른 클래스로 분리되어 있다.

- 다른 클래스에서 Validation 정책을 사용할 수 있다.

- 실행시간에 Validation 정책을 교체할 수 있다.

 

Edit 예제의 경우는 strategy가 더 적합하지만 template method 자체가 나쁜 것은 절대 아니다.

- 앞서 다뤘던, 도형 편집기에서 사각형 그리는 방법은 다른 클래스에서 사용해야 될 일이 없다.

- 또한 실행 시간에 교체할 이유도 없으며, 가상함수로 구현되면 멤버 함수라서 멤버 데이터 접근도 편리하다.

'C++ > Design Pattern' 카테고리의 다른 글

[C++ Design Pattern] Template Method Pattern  (0) 2024.09.18
Comments