03. 패턴
3.0. 패턴의 종류
- 생성 패턴
추상 팩토리 : 연관된 팩토리 그룹을 캡슐화 한다.
빌더 : 복잡한 객체의 표현 방법과 구조를 분리 한다.
팩토리 메서드 : 클래스의 인스턴스 생성을 서브 클래스로 넘긴다.
프로토 타입 : 새로운 객체를 생성하기 위해 복제 가능한 클래스의 인스턴스 프로토 타입을 명시한다.
싱글톤 : 오직 하나의 클래스 인스턴스만을 생성한다.
- 구조 패턴
어댑터 : 한 클래스의 인터페이스를 다른 인터페이스로 변경한다.
브릿지 : 구현된 추상화를 독립적으로 변경시킬수 있도록 분리한다.
컴포지트 : 부분-전체 계층을 표현하기 위해 객체를 트리 구조로 구성한다.
데코레이터 : 이미 존재하는 객체에 동적으로 행동을 추가 한다.
퍼사드 : 서브 시스템의 인터페이스에 일관된 고수준의 인터페이스를 제공한다.
플라이급 : 여러 가지의 작은 객체들을 효과적으로 지원하기 위해 공유를 사용한다.
프록시 : 다른 객체의 접근을 제어하기 위한 대처 방법을 제공한다.
- 행동 패턴
책임 연쇄 : 송신 객체의 요청이 처리될 수 있도록 하나 이상의 수신 객체를 제공한다.
커맨드 : 객체 형태로 요청이나 연산을 캡슐화하고 연산 취소를 지원한다.
인터프리터 : 주어진 언어로 어떻게 문장을 표현하고 평가하는지를 명시한다.
이터레이터 : 객체에 저장된 여러 요소들을 순차적으로 접근할 수 있는 방법을 제공한다.
메디에이터 : 객체가 어떻게 상호작용하는지를 캡슐화 하는 객체를 정의한다.
메멘토 : 나중에 같은 상태로 객체가 다시 저장될 수 있도록 해당 객체의 내부 상태를 캡처한다.
옵저버 : 1대 n 객체 관계에서 상태 변경 알림을 처리한다.
스테이트 : 객체의 내부 상태가 변경되면 자신의 형태를 변경하도록 허용한다.
스트레티지 : 같은 계열의 알고리즘을 정의해서 각각을 캡슐화 한 후 런타임에 서로 교환 가능하도록 만든다.
템플릿 메서드 : 연산의 알고리즘 뼈대를 정의하고 일부 단계는 서브클래스에서 처리 하도록 넘긴다.
방문자 : 객체 구조를 이루는 요소에 대해 수행할 연산을 표현한다.
디자인 패턴의 종류는 대략 위의 내용과 같다. 이 책에는 고품질 API를설계하는 데 반드시 필요한 특정 디자인 패턴에 초점을 맞추어 설명한다.
Piml 이디엄 : private 멤버 데이터와 함수를 .cpp 파일에 위치 시키기 때문에 잘 보호된 API를 만든다.
싱글톤과 팩토리 메서드 : 싱글톤은 오로지 객체의 한 인스턴스만을 생성할 때 사용하는 유용한 패턴, 팩토리 메서드 패턴은 객체의 인스턴스를 생성하는 일반화된 방법을 제공하며 상속된 클래스의 세세한 구현 코드를 감출 수 있다.
프록시, 어댑터와 퍼사드 : 프록시와 어댑터는 이미 존재하는 클래스와 새로운 클래스를 일대일 매핑 시킨다. 퍼사드는 수많은 클래스의 인터페이스를 단순하게 만든다.
옵져버 : 느슨하게 연결된 API 설계시 중요
3.1 Piml 이디엄( pointer to implementation : 구현 포인터 )
: "이미 선언한 타입의 포인터를 가리키는 C++ 클래스의 데이터 멤버를 정의할 수 있다."라는 개념으로 pimpl은 private 데이터 멤버와 함수를 논리적으로 그리고 물리적으로 감추는 방법
//pimpl 헤더파일에는 선언만 하고 그 구현코드는 .cpp 파일의 클래스 안에 모든 private 멤버를 추가한다.
//autotimer.h
class AutoTimer
{
public :
explicit AutoTimer( const std::string &name );
~AutoTimer();
private :
class Impl;
Impl *mImpl;
}
구현 코드를 숨긴 간결한 API를 구성하지만 비용 측면에서는 큰 혜택을 얻을 수 없다.
private 선언은 AutoTimer 클래스의 메서드만이 Impl 클래스의 멤버에 접근 할 수 있다는 제약조건을 만들기 때문에, .cpp 파일에 정의한 다른 클래스나 함수들은 Impl 클래스에 접근할 수 없다. 하지만 이 방법이 너무 많은 제약을 수반할 경우, 아래의 예제 코드처럼 Impl을 중첩된 public 클래스로 선언할 수도 있다.
//autotimer.cpp
class AutoTimer::Impl
{
public :
double GetElasped() const
{
...
}
std::string mName;
DWORD mStartTime;
...
};
AutoTimer::AutoTimer( const std::string &name ):mImpl(new AutoTimer::Impl())
{
mIpl -> mName = name;
...
}
AutoTimer::~AutoTimer()
{
delete mImpl;
mImpl = NULL;
}
위 설계를 보면 Impl 클래스를 AutoTimer 클래스 안에 중첩된 private 클래스로 선언했다. Impl 클래스를 중첩된 클래스로 선언하면 구현부에 명시된 심벌을 통해 전역 네임 스페이스의 혼선을 피하고, private로 선언하는 것 역시 public API를 뚜렷하게 구분한다.
private 선언은 AutoTimer 클래스의 매서드만 Impl 클래스의 멤버에 접근할 수 있는 제약조건있기 때문에 public을 통해 접근을 허용하기도 한다.
class AutoTimer
{
public :
...
class Impl; //AutoTimer.cpp에 정의된 다른 클래스나 함수들이 접근할 수 있도록 허용
private :
Impl *mImpl;
}
Tip. Pimpl 이디엄을 사용할 때는 구현 클래스를 중첩된 private으로 선언해서 사용한다. 반드시 .cpp 파일에 정의된 다른 클래스 또는 함수가 Impl 클래스의 멤버에 접근해야 할 경우에만 Impl 클래스를 중첩된 public 으로 선언해서 사용한다.
설계시에도 충분히 고려해야 할 사항은 얼마나 많은 로직을 Impl클래스에 구현해야 하는 가 이다.
1. private 멤버 변수만을 포함한다.
2. private 멤버 변수와 메서드를 포함한다.
3. public 클래스의 모든 메서드와 대응되는 메서드를 Impl 클래스에 구현한다.
보통 Impl 클래스에 모든 private 변수와 모든 private 메서드를포함하길 권장한다.
3.1.2 의미론적 복사
컴파일러가 만든 기본 생성자는 얕은 복사(shallow copy)만 수행하기 때문에 Pimpl 클래스의 경우 문제가 발생한다. 따라서 복사 문제를 해결하기 위해선, 클래스 복사를 제한(private 선언 혹은 boost::noncopyable 적용)하거나, 의미론적 복사 기능을 명시적으로 정의(deep copy) 한다.
3.1.3 Pimpl과 스마트 포인터
class AutoTimer
{
public :
explicit AutoTimer( const std::string &name );
~AutoTimer ();
private :
class Impl;
boost::scoped_ptr<Impl> mImpl;
}
Pimpl 클래스의 복사에 대해 생각하고 구현 포인터의 초기화와 소멸을 쉽게 관리할 수 있도록 스마트 포인터 사용을 하자.
또한 boost::shared_ptr을 사용한다면 이중으로 객체를 제거하는 문제를 발생시키지 않고 객체를 복사할 수 있다.
3.1.4 Pimpl의 장점
- 정보 은닉 : private 선언과 인터페이스 노출을 간소화 함
- 의존성 제거 : 헤더파일에서 멤버변수등을 제거하여 .cpp로 옮기므로 의존성 결합 요소를 제거할 수 있다.
- 빠른 컴파일 : .cpp로 이동시켜 API의 계층 구조가 줄어든다.
- 뛰어난 이진 호환성 : Pimpl 객체의 크기가 바뀌지 않는다. 모든 변경사항은 .cpp 파일에 캡슐화된 구현 클래스의 크기에만 영향을 미치기 때문에 객체의 이진 표현을 변경할 필요 없이 중요한 구현 코드를 변경할 수 있다.
- 지연 할당 : mImpl 클래스는 필요할 때 생성되기 때문에 제한된 혹은 비용이 많이 드는 리소스 사용시 유용하다.
3.1.5 Pimpl의 단점
: 생성된 모든 객체는 사용하는 별도의 구현 객체를 메모리에 할당하고 해제해야 한다. 이로 포인터의 크기 증가 간접 참조의 성능문제, new, delete 호출에 추가적 비용이 발생한다.
상수 메서드 안에 있는 변수들은 분리된 객체 안에 존재하기 때문에 컴파일러는 이 변수들의 값 변화를 더 이상 감지할 수 없다. 때문에 컴파일러는상수메서드 안에서 mImpl 포인터를 통해 접근하는 변수의 값이 변경되는지 여부만을 체크한다.
cf) C에서도 Opaque 포인터를 만들어 사용할 수 있다. 이는 .c 확장자가 갖는파일 안에서만 정의되는 구조체에 포인터를 생성하는 것과 같은 원리다.
3.2 싱글톤
오직 하나의 인스턴스만을 생성할 수 있도록 보장하며 전역에서 하나의 인스턴스에 접근할 수 있는 접근 포인트도 제공한다.
Tip. 클래스를 완전한 싱글톤으로 유지하려면 생성자, 소멸자, 복사 생성자와 할당 연산자를 private( 혹은 protected )으로 선언하라.
3.2.2 스레드에 안전한 싱글톤 만들기
GetInstance()메서드는 스레드에 안전하지 않다. 두 개의 스레드가 동시에 호출하면 경합이 발생될 수 있기 때문이다.
//컴파일러에 의해 확장된 GetInstance()
Singleton &Singleton::GetInstance()
{
//컴파일러 생성 예제 코드
extern void __DestructSingleton();
static char __buffer[sizeof(Singleton)];
static bool __initialized = false;
if( !__initialized )
{
new(__buffer ) Singleton(); //새로운 신택스
atexit( __DestructSingleton ); //종료 시점에 객체 소멸
__initialzed = true;
}
return *reinterpret_cast<Singleton *>(__buffer);
}
//경합을 방지하기 위해서 코드를 뮤텍스 잠금으로 감싸 스레드 안전문제를 해결할 수 있다.
Singleton &Singleton::GetInstance()
{
Mutex mutex;
ScopedLock(&mutex); //함수 실행 종료 시점에서 뮤텍스 잠금 해제
static Singleton instance;
return instance;
}
위 방식은 호출시마다 잠금으로 인한 비용이 든다.
다른 해결방법으로는 이중 체크 잠금 패턴(DCLP, Double Check Locking Pattern)이 있다.
//이중 체크 잠금 패턴(DCLP)
Singleton &Singleton::GetInstance()
{
static Singleton *instance = NULL;
if( !instance ) //1번째 체크
{
Mutex mutex;
ScopedLock(&mutex);
if( !instance ) //2번째 체크
{
instance = new Singleton();
}
}
return *instance;
}
위 방식도 공유 메모리 대칭 멀티 프로세서에서는 한 번에 여러 개의 쓰기 작업을 수행하기 때문에 문제가 발생할 수 있다. 이때는 volatile 키워드로 방지할 수 있다.
하지만 완벽히 막기는 어렵다 때문에 이 문제를 우회하기 위해 상황에 따라 플랫폼에 명시적인 메모리 장벽을 사용하거나 POSIX 스레드만을 사용하는 경우 pthread_once() 함수를 사용하는 것도 방법이다.
cf) 메모리 장벽 : 코드 지연 없이코드를 동기화 시키는 방법
cf) POSIX : 이식 가능 운영 체제 인터페이스
스레드에 안전하게 만든 GetInstance()메서드의 성능이 문제가 된다면 앞에서 말한 지연된 인스턴스(lazy instantiation)모델 대신 코드가 실행되는 시점, main() 호출 전이나 뮤텍스 잠금으로 구현한 API가 초기화 되는 시점에서 싱글톤을 초기화 하는 것도 방법이다.
1. 정적 초기화 : 일반적인 하나의 스레드 실행 시 main()보다 static가 먼저 호출 된다.
static Singleton &foo = Singleton::GetInstance();
2. 명시적 API 초기화 : 라이브러리에 별도의 초기화 로직을 추가하는 방법
void APIInitialize()
{
Mutex mutex;
ScopedLock( &mutex );
Singleton::GetInstance();
}
Tip. C++에서 스레드에 안전한 싱글톤 인스턴스를 만드는 것은 쉽지 않다. 그렇기 때문에 이때는 정적 생성자 또는 API 초기화 함수를 통해서 문제를 해결하는 방법을 생각해보자.
3.2.3 싱글톤 vs 의존성 삽입(dependency injection)
의존성 삽입(dependency injection)이란 객체를 클래스에서 생성하고 보관하기보다는 클래스에 삽입하는 기술
class MyClass {
//Database의 정보가 변경될 경우 코드를 수정해야 하는 문제가 발생
//MyClass() : mDatabase( new Database("mydb", "localhost", "user", "pass")) {}
//의존성 삽입. 생성에 대한 고민을 줄이며, Database객체만 전달하면 된다.
MyClass(Database * db) : mDatabase(db) {}
private :
Database *mDatabase;
};
3.2.4 싱글톤 vs 모노스테이트
싱글톤 객체의 초기화 문제나 상태 유지에 관여할 필요가 없다면 모노스테이트 디자인 패턴을 사용하는 것이 좋다.
모노스테이트 패턴의 장점
- 여러 개의 인스턴스를 생성할 수 있다.
- GetInstance() 메서드가 특별히 필요하지 않기 때문에 투명한 사용성을 제공한다.
- 정적 변수 사용을 통해 잘 정의된 생성과 소멸의 의미를 보여준다.
Tip. 전역 데이터의 지연된 초기화가 필요없거나 하나의 클래스를 투명하게 사용하고 싶다면 싱글톤 대신 모노스테이트의 사용을 고려해 보자.
//monostate.h
class Monostate
{
public :
int GetTheAnswer() const { return sAnswer; }
private :
static int sAnswer;
}
//monostate.cpp
int Monostate::sAnswer = 42;
모노스테이드 패턴은 한가지 단점을 가지고 있다.
정적 멤버 함수는 가상 함수가 될 수 없기 때문에 정적 메서드를 재정의하는 메서드를 사용할수 없다는 점이다. 또한 더 이상 클래스의 인스턴스도 생성할 수 없기 때문에 초기화 또는 메모리 해제 코드를 추가할 수 없다.
3.2.5 싱글톤 vs 세션 상태
싱글톤은 전역 데이터를 저장히기 위한 가장 기본적인 방법이면서 동시에 잘못된 설계의 지표로 사용되는 경향이 있다. 이유는 요구사항이 점점 변경되어, 코드도 바뀌고 여러개의 인스턴스를 지원해야 하는 상황에 처하기 때문이다.
싱글톤 패턴을 대체하는 방법으로 의존성 삽입이나 모노스테이트 패턴, 세션 문맥 사용등의 방법이 있다.
3.3 팩토리 메서드
생성 디자인 패턴의 한 종류로 c++의 특정한 타입을 명시하지 않고도 객체를 생성한다. 기본적으로, 팩토리 메서드는 단순히 클래스의 인스턴스를 생성하기 위한 일반적인 메서드이면서 동시에 상송 클래스가 팩토리 메서드를 오버라이드해서 자신의 인스턴스를 리턴시키는 상속 구조에서 주로 사용된다.
또한, 추상 기본 클래스를 사용해서 팩토리를 구현하기도 한다.
3.3.1 추상 기본 클래스
하나 이상의 그 순수 가상 함수들로 구성되며, new 연산자를 통해 구체화하거나 인스턴스화 시킬 수 없다. 대신 자신이 가진 가상 함수를 상속된 클래스에서 구현하도록 한다.
하나 이상의 가상 메서드를 가진 어떤 클래스를 사용할 때면 반드시 추상 클래스의 소멸자를 가상으로 선언해야 한다.
class IRenderer
{
//가상소멸자 선언하지 않음
virtual void Render() = 0;
};
class RayTracer : public IRenderer
{
RayTracer();
~RayTracer();
void Render(); //추상 기본 클래스 메서드의 구현
};
int main(int, char **)
{
IRender *r = new RayTracer();
//RayTracer::~RayTracer가 아닌 IRenderer::~IRenderer 소멸자 호출 deleter;
}
3.3.2 단순한 팩토리 예제
사용자의 입력값에 따라, 사용자가 원하는 인스턴스를 런타임시에 제공한다.
//renderfactory.cpp
#include "rendererfactory.h"
#include "openglrenderer.h"
#include "directxrenderer.h"
#include "mesarenderer.h"
IRenderer *RendererFactory::CreateRenderer( const std::string &type )
{
if( type == "directx" )
return new DirectXRender();
if( type == "mesa" )
return new MesaRender();
return NULL;
}
3.3.3 확장 가능한 팩토리 예제
팩토리 메서드와 상속 클래스 간의 연결 관계를 느슨하게 만들고 런타임 시에 새로운 상속 클래스를 추가할 수 있도록, 타입 이름을 객체 생ㅅㅇ 콜백 함수에 연결시키는 맵을 팩토리 클래스에서 사용할 수 있다.
#include "renderer.h"
class RendererFactory
{
public :
typeofdef IRenderer *(*CreateCallback)();
static void RegisterRenderer( const std::string &type, CreateCallback cb );
static void UnregisterRenderer( const std::string &type );
static IRenderer *CreateRenderer( const std::string &type );
private :
typedefofdef std::map<std::string, CreateCallback > CallbackMap;
static CallbackMap mRenderer;
};
//cpp
RendererFactory::CallbackMap RendererFactory::mRenderers;
void RendererFactory::RegisterRenderer( const std::string &type, CreateCallback cb )
{
mRenderer[type] = cb;
}
void RendererFactory::UnregisterRenderer( const std::string &type)
{
mRenderers.erase( type );
}
IRenderer *RendererFactory::CreateRenderer( const std::string &type )
{
CallbackMap::iterator it = mRenderers.find( type );
if( it != mRenderer.end() )
{
//상속 클래스를 생성하기 위한 생성 콜백 함수 호출
return ( it->second());
}
return NULL;
}
class UserRenderer : public IRenderer
...
//새로운 그래픽 처리기 등록
RendererFactory::RegisterRenderer( "user", UserRenderer::Create );
//새로운 그래픽 처리기의 인스턴스 생성
IRenderer *r = RendererFactory::CreateRenderer( "user" );
r->Render();
3.4 API 래핑 패턴
규모가 큰 레거시 시스템을 연동하는 작업의 경우 관련 코드를 모두 변경하는 방법보다는 기존의 레거시 코드를 기반으로 새로운 인터페이스를 API로 노출시키는 설꼐 방법이 효과적이다.
래퍼 API사용의 단점은, 간접적인 추가 계층으로 인해 잠재적 성능상의 문제와 래퍼 수준에서 저장하는 추가 상태 정보들로 인한 오버헤드가 있다.
3.4.1 프록시 패턴
일대일의 인터페이스, 클래스 전달 방식을 제공한다.
프록시 클래스에 있는 FuncA()는 원래 클래스에 있는 FuncA()를 호출한다. 따라서 프록시 클래스와 원래 클래스는 같은 인터페이스를 갖는다.
이 패턴은 주로 기존 클래스의 복사본을 프록시 클래스에 저장하거나 아니면 포인터를 저장하는 방식으로 구현한다.
class Proxy
{
public :
Proxy() : mOrig(new Original()) {}
~Proxy() { delete mOrig; }
bool DoSomting(int value)
{
return mOrig->DoSomething(value);
}
private : //복사 연산자와 할당 연산자 제한
Proxy( const Proxy& );
const Proxy &operator = (const Proxy & );
Original *mOrig;
}
또 다른 방법은 프록시와 원형 API가 함께 사용하는 추상인터페이스를 통해 접근 방식을 늘리며, 이를 통해 변경 상황이 발생해도 API 동기화가 편리하다.
class IOriginal
{
public :
virtual bool DoSomething( int value )= 0;
};
class Original : public IOriginal
{
public :
bool DoSomething( int value );
};
class Proxy : IOriginal
{
public :
Proxy() : mOrig( new Original() ) {}
~Proxy() { delete mOrig; }
bool DoSomething( int value ) { return mOrig->DoSomething( value ); }
private :
Proxy( const Proxy& );
const Proxy &operator = ( const Proxy& );
Original *mOrig;
};
Tip. 프록시는 같은 형태의 인터페이스를 가진 객체의 함수를 호출하는 인터페이스를 제공한다.
프록시 패턴은 인터페이스의 형태를 변경하지 않고 원형 클래스의 행동을 변경할 때 유용하며, 원형 클래스가 서드 파티 라이브러리인 경우 더욱 유용하다.
프록시 패턴의 유즈 케이스
1. 원형 객체의 지연된 인스턴스 구현 : 실제 메서드 호출 전까지 인스턴스화 하지 않는다.
2. 원형 객체의 접근 제어 구현 : 원형 객체와 프록시 사이에 권한 계층을 두어 제어
3. 디버그 또는 "드라이 런" 모드 지원
4. 스레드에 안전한 원형 객체 지원 : 뮤텍스 잠금 추가
5. 자원 공유 지원 : 같은 원형 객체를 사용하는 여러 개의 프록시 객체
6. 원형 객체의 향후 변경으로 부터 보호 : 향후 변경될 것을 예상하여 원형 객체의 동작을 그대로 옮겨 구현하며, 변경 시 기존 인터페이스는 유지하고 새로운 라이브러리의 구현 부분 변경(어댑터 사용)
cf) 드라이 런(dry run) : 테스트 프로세스 중 하나로, 테스트 환경에서 실제로 어떤 코드가 어느 시점에서 무엇을 하는지를 한 번에 하나씩 살펴보는 테스트 방법이다.
3.4.2 어댑터 패턴
하나의 클래스가 구현하는 인터페이스를 다른 인터페이스와 호환 되도록 만든다. 프록시와 유사하지만 어댑터 클래스와 원형 클래스 간의 인터페이스는 다를 수 있다.
class RectangleAdapter
{
public :
RectangleAdapter():mRect( new Rectangle()) {}
~RectangleAdapter() { delete mRect; }
void Set(float x1, float y1, float x2, float y2)
{
... //크기 설정
}
private :
RectangleAdapter( const RectangleAdapter &);
const RectangleAdapter &operator = (const RectangleAdapter &);
Rectangle *mRect;
};
RectangleAdapter 클래스는 다른 메서드의 이름을 사용해서 크기를 설정하는 Set()을 호출한다. RectangleAdapter는 Rectangle를 상속한다. 만약 어댑터에서 부모 클래스의 인터페이스를 노출하고 싶으면 public을 통해 부모 클래스의 API를 그대로 노출할 수 있다.
Tip. 어댑터는 하나의 인터페이스를 호환 가능한 인터페이스로 변경할 뿐이지, 형태가 다른 인터페이스와는 함께 사용할 수 없다.
어댑터 패턴의 장점
1. API 일관성 유지 : 서로 다른 인터페이스의 클래스들을 모아 일관성 있는 인터페이스 제공
2. API에서 사용하는 라이브러리 감싸기
3. 데이터 타입 변경 : 서로 다른 인터페이스의 데이터를 변환하여 사용한다.
4. 다른 명명 규칙 사용 가능 : C로 작성된 API를 위해 C++클래스에서 C함수를 호출하는 어댑터 클래스를 제공할 수 있다.
3.4.3 퍼사드 패턴
커다른 클래스의 단순화된 이너페이스를 제공한다. 퍼사드 패터은 기본이 되는 서브 시스템을 좀 더 쉽게 사용할 수 있는 고수준의 인터페이스를 정의한다. 퍼사드 패턴은 다중 컴포넌트 래퍼의 한 가지 예라 할 수 있지만 어댑터와는 다르다. 왜냐하면 퍼사드는 클래스의 구조를 단순화 시키기 때문이다.
퍼사드는 public 인터페이스와 기반 서브 시스템이 서로 접근할 수 없도록 분리시킬 수 있으며 이 경우를 보통 "퍼사드 캡슐화"라고 부른다. 퍼사드 캡슐화를 통해서 인터페이스가 기반 클래스에 접근할 수 없게 만들 수 있다.
//예) 일반적인 여행 확인 사항
class Taxi
{
bool BookTaxi( int npeople, time_t pickip_time );
};
class Restaurant
{
bool ReserveTable( int npeople time_t arrival_time );
};
class Therater
{
time_t GetShowTime();
bool ReserveSeats( int npeople, int tier );
}
//퍼사드 패턴을 적용하여 인터페이스 단순화
class ConciergeFacade
{
public :
enum ERestaurant {
RESTAURANT_YES,
RESTAURANT_NO
};
enum ETaxi {
TAXI_YES,
TAXI_NO
};
time_t BookShow( int npeople, ERestaurant addRestaurant, ETaxi addTaxi );
};
3.5 옵저버 패턴
옵저버 패턴은 다른 객체 안에 있는 메서드를 호출하는 가장 일반적인 방법이다.
...
0 개의 댓글:
댓글 쓰기