Design pattern in C++ - 공통성과 가변성의 분리[2]
이번시간에는 변하는 것을 분리하는 방법, 단위 전략 디자인, 상태 패턴 등에 대해 알아보겠습니다.
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가지
- 인터페이스 활용 : Strategy, State pattern
- 실행시간 교체 가능, 가상함수 기반(느림)
- 템플릿 인자 활용 : Policy base design
- 실행시간 교체 불가, 인라인 치환 가능(빠른 성능)
- 인터페이스 활용 : Strategy, State pattern
- 가상 함수로 분리(template method) 하는 방법