이번시간에는 변하는 것을 분리하는 방법, 단위 전략 디자인, 상태 패턴 등에 대해 알아보겠습니다.

1. 변하는 것을 분리하는 방법

변하는 것을 가상함수로(상속 기반, template method)

  • 변하지 않는 코드(전체 흐름) 안에 있는 변해야 하는 부분(정책)은 분리하는 것이 좋다
    • 변해야 하는 부분(아래 코드에서는 유효성 체크 부분)을 별도의 가상함수로 분리
    • 변하는 것을 가상함수로 분리 할 때의 장점
      • 파생 클래스를 만들어 가상함수를 재정의 하면 됨
#include <iostream>
#include <string>
#include <conio.h>
using namespace std;

class Edit
{
    string data;
public:

    virtual bool validate(char c)
    {
        return isdigit(c);
    }

    string getData()
    {
        data.clear();

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

            if ( c == 13 ) break;

            if ( validate(c) )
            {
                data.push_back(c);
                cout << c;
            }
        }
        cout << endl;
        return data;
    }
};
//--------------------
class AddressEdit : public Edit
{
public:
    virtual bool validate(char c)
    {
        return true;
    }
};

int main()
{
    AddressEdit edit;

    while(1)
    {
        string s = edit.getData();
        cout << s << endl;
    }
}

변하는 것을 다른 클래스로(포함 관계, Strategy pattern)

  • 인터페이스를 기반으로 약한 결합 처리
#include <iostream>
#include <string>
#include <conio.h>
using namespace std;

// validation 을 위한 인터페이스
struct IValidator
{
    virtual bool validate(string s, char c) = 0;
    virtual bool iscomplete(string s)  { return true;}

    virtual ~IValidator() {}
};

// 주민 번호 : 801    1   확인.
class Edit
{
    string data;
    //---------------------
    IValidator* pVal = 0;
public:
    void setValidator(IValidator* p ) { pVal = p;}
    //---------------------------

    string getData()
    {
        data.clear();

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

            if ( c == 13 &&
                ( pVal == 0 || pVal->iscomplete(data)  ) ) break;

            if ( pVal == 0 || pVal->validate(data, c) )
            {
                data.push_back(c);
                cout << c;
            }
        }
        cout << endl;
        return data;
    }
};

class LimitDigitValidator : public IValidator
{
    int value;
public:
    LimitDigitValidator(int n) : value(n) {}

    virtual bool validate( string s, char c )
    {
        return s.size() < value && isdigit(c);
    }

    virtual bool iscomplete( string s)
    {
        return s.size() == value;
    }
};

int main()
{
    Edit edit;
    LimitDigitValidator v(5);
    edit.setValidator(&v);

    while(1)
    {
        string s = edit.getData();
        cout << s << endl;
    }
}

Template method vs strategy pattern 장단점

  • Strategy 패턴
    • 정책을 재사용 할 수 있고, 런타임에 정책을 교체 할 수 있음
    • 인터페이스를 통해 독립적으로 알고리즘을 변경 할 수 있음
  • Template method
    • 알고리즘의 구조를 유지하면서, 동작의 일부를 파생 클래스에서 처리하도록 구현 가능(hook method)
    • 유연성이 떨어짐(런타임에 전략 교체 불가, 상속 관계로 구현하기 때문)

2. Policy base design

Policy base design(단위 전략 디자인)

  • template 인자를 사용해서 정책 클래스를 교체하는 기법
  • 사용자에게 특정 기능을 선택하도록 제공
    • 동기화 버전 list 제공 예제
#include <iostream>
using namespace std;


template<typename T, typename ThreadModel = NoLock> class List
{
    ThreadModel tm; // 동기화 정책을 담은 클래스
public:
    void push_front(const T& a)
    {
        tm.Lock();
        //...
        tm.Unlock();
    }
};

class MutexLock
{
public:
    inline void Lock()   { cout << "mutex lock" << endl;}
    inline void Unlock() { cout << "mutex Unlock" << endl;}
};
class NoLock
{
public:
    inline void Lock()   {}
    inline void Unlock() {}
};

List<int, MutexLock> s;

int main()
{
    s.push_front(10);
}
  • 단위 전략 vs Strategy 패턴 비교
구 분 속도 유연성
전략 패턴 가상 함수 기반, 느림 런타임 정책 교체 가능
단위 전략 패턴 인라인 치환 가능, 빠름 컴파일 타임 결정(런타임 X)

Application Framework

각 분야의 프로그램은 전체적인 흐름이 항상 유사

  • GUI, 게임, 앱 등

main 함수에 전체적인 흐름을 담아서 라이브러리 내부에 감추자 -> application framework

  • MFC, IOS, Android 등등..
#include <iostream>
using namespace std;


class CWinApp; // 클래스 전방 선언.

CWinApp* g_app = 0;

class CWinApp
{
public:
    CWinApp()    { g_app = this;}
    virtual bool InitInstance() { return false; }
    virtual int  ExitInstance() { return false; }
    virtual int  Run()          { return 0; }
};

int main()
{
    if ( g_app->InitInstance() == true)
        g_app->Run();
    g_app->ExitInstance();
}

//-----------------------
// 라이브러리 사용자
// 1. CWinApp 의 파생 클래스 만들어야 한다.
// 2. 사용자 클래스를 전역객체 생성
class MyApp : public CWinApp
{
public:
    virtual bool InitInstance()
    {
        cout << "initialization" << endl;
        return true;
    }

    virtual int  ExitInstance()
    {
        cout << "finish" << endl;
        return 0;
    }
};

MyApp theApp;

함수와 정책(변해야 할 수 있는 정책은 인자화)

  • 정렬 정책을 함수포인터로 전달
#include <iostream>
#include <algorithm>
using namespace std;

void Sort(int* x, int sz, bool(*cmp)(int, int))
{
    for (int i = 0; i < sz-1; i++)
    {
        for( int j = i + 1; j < sz; j++)
        {
            if ( cmp( x[i],  x[j])  )
                swap( x[i], x[j] );
         }
     }
}

bool cmp1( int a, int b) { return a < b;}
bool cmp2( int a, int b) { return a > b;}
int main()
{
    int x[10] = { 1,3,5,7,9,2,4,6,8,10};

    Sort( x, 10, &cmp2);

    for ( auto n : x)
        cout << n << ", ";
}
  • 템플릿(인라인 치환을 위해) + 람다 표현식으로 정책 전달
#include <iostream>
#include <algorithm>
using namespace std;

template<typename T>
void Sort(int* x, int sz, T cmp)
{
    for (int i = 0; i < sz-1; i++)
    {
        for( int j = i + 1; j < sz; j++)
        {
            if ( cmp( x[i],  x[j])  )
                swap( x[i], x[j] );
         }
     }
}

bool cmp1( int a, int b) { return a < b;}
bool cmp2( int a, int b) { return a > b;}

int main()
{
    int x[10] = { 1,3,5,7,9,2,4,6,8,10};
    Sort( x, 10, [](int a, int b) { return a > b;});

    for ( auto n : x)
        cout << n << ", ";
}

3. State pattern

특정 조건에 따라 다른 처리를 해야 할 때 분기문(if, switch case)을 사용 할 경우, 조건이 추가 될 경우 분기문의 양이 늘어나는 문제가 발생

#include <iostream>
using namespace std;

class Character
{
    int gold = 0;
    int item = 0;
public:
    void run()
    {
        if ( item == 1)
            cout << "run"    << endl;
        else if ( item == 2 )
            cout << "fast run" << endl;
    }
    void attack()
    {
        if ( item == 1)
             cout << "attack" << endl;
        else if ( item == 2 )
             cout << "attack2" << endl;
    }
};

int main()
{
    Character* p = new Character;
    p->run();
    p->attack();
}
  • 해결 방법1 : 변하는 부분을 가상함수로 처리
    • 모든 동작을 가상함수화
    • 기존 객체의 동작이 변경된 것이 아닌, 동작이 변경된 새로운 객체가 생성 된 것
    • 객체의 변화가 아닌 클래스에 의한 변화
#include <iostream>
using namespace std;

class Character
{
    int gold = 0;
    int item = 0;
public:
    void run()    { runImp(); }
    void attack() { attackImp(); }

    virtual void runImp() {}
    virtual void attackImp() {}
};

class PowerItemCharacter : public Character
{
public:
    virtual void runImp() { cout << "fast run" << endl;}
    virtual void attackImp() { cout << "attack" << endl;}
};

class NormalCharacter : public Character
{
public:
    virtual void runImp() { cout << "run" << endl;}
    virtual void attackImp() { cout << "power attack" << endl;}
};

int main()
{
    Character* p = new NormalCharacter;
    p->run();
    p->attack();

    p = new PowerItemCharacter;
    p->run();
    p->attack();
}
  • 해결 방법2 : State pattern 사용

    • 상태에 따라 변경되는 동작들을 다른 클래스로 분리

    • 동작의 인터페이스를 정의

#include <iostream>
using namespace std;

struct IState
{
    virtual void run() = 0;
    virtual void attack() = 0;
    virtual ~IState() {}
};

class Character
{
    int gold = 0;
    int item = 0;
    IState* state;
public:
    void changeState(IState* p) { state = p;}
    void run()    { state->run(); }
    void attack() { state->attack(); }
};

class NormalState : public IState
{
    virtual void run()   { cout << "run" << endl;}
    virtual void attack(){ cout << "attack" << endl;}
};
class PowerItemState : public IState
{
    virtual void run()   { cout << "fast run" << endl;}
    virtual void attack(){ cout << "power attack" << endl;}
};

int main()
{
    NormalState ns;
    PowerItemState ps;

    Character* p = new Character;
    p->changeState(&ns);
    p->run();
    p->attack();
    
    p->changeState(&ps); // 아이템 획득.
    p->run();
    p->attack();
}

State pattern 정리

  • 객체 자신의 내부 상태에 따라 행위(동작)을 변경하도록 처리 객체는 마치 클래스를 바꾸는 것 처럼 보임
  • 앞서 살펴본 Strategy pattern(전략 패턴)과 클래스 다이어그램 및 구조는 동일
    • 전략 패턴은 알고리즘 교체의 의미
    • State 패턴은 동작 교체의 의미

4. Summary

변하지 않는 코드에서 변해야 하는 부분을 분리

  • 일반 함수에서 변하는 것 -> 함수 인자로 분리(함수 포인터 or 함수 객체 or 람다)
  • 멤버 함수에서 변하는 것
    • 가상 함수로 분리(template method) 하는 방법
      • 실행 시간에 교체 X, 코드 재사용 불가
      • 상속 기반 패턴
    • 다른 클래스로 분리 하는 방법
      • 변하는 코드(정책) 재사용 가능
      • 교체 방법 2가지
        1. 인터페이스 활용 : Strategy, State pattern
          • 실행시간 교체 가능, 가상함수 기반(느림)
        2. 템플릿 인자 활용 : Policy base design
          • 실행시간 교체 불가, 인라인 치환 가능(빠른 성능)