클래스 하나를 인스턴스 별로 다른 객체형으로 표현할 수 있게 만들어, 새로운 '클래스들'을 유연하게 만들 수 있게 한다.

 

Zombie - Monster, Dragon - Monster는 is-a 관게이다. 이런 관계를 전통적인 OOP에서는 클래스 상속으로 구현한다.

몬스터 종족의 수가 많아지면 코드를 추가, 수정하는데 비용이 많이 들게 된다.

 

Monster 클래스에서 종족 정보를 담는 Breed 클래스를 참조하게 만들면 해결할 수 있다.

Breed 객체는 설정 파일(json)에서 읽은 데이터로 생성한다.

 

타입 객체 패턴은 코드 수정 없이 새로운 타입을 정의할 수 있다는 게 장점이다. 코드에서 클래스 상속으로 만들던 타입 시스템의 일부를 런타임에 정의할 수 있는 데이터로 옮긴다.

 

13.3 패턴

타입 객체 클래스(Breed)와 타입 사용 객체 클래스(Monster)를 정의한다.

인스턴스 별로 다른 데이터는 타입 사용 객체 인스턴스에 저장하고,

개념적으로 같은 타입끼리 공유하는 데이터나 동작은  타입 객체에 저장한다.

 

13.4 언제 쓸 것인가?

나중에 어떤 타입(새로운 몬스터 종족)이 필요할지 알 수 없을 때

컴파일이나 코드 변경 없이 새로운 타입을 추가하거나 타입을 변경하고 싶을 경우

 

13.5 주의사항

타입 객체 패턴를 직접 관리해야 한다

상속 방식을 사용했을 때는 종족 클래스를 정의하는 데이터가 컴파일될 때 포함되는데,

타입 객체 패턴을 사용하면 종족 객체를 직접 만들고 초기화시켜 사용 객체 클래스(Monster)에 넘겨줘야 한다.

 

타입별로 동작을 표현하기가 더 어렵다

상속 방식에서는 메서드를 오버라이드해서 코드로 값을 계산하거나 다른 코드를 호출하는 등 마음대로 할 수 있다.

 

타입 객체 패턴에서는 종족 객체 변수에 데이터를 저장하는 방법으로 타입을 설정하기 때문에 동작을 정의하기는 어렵다.

우회적인 방법으로 바이트코드 패턴(11장)에서 하던 것처럼 동작 코드를 정의해놓은 뒤에 타입 객체 데이터에서 이 중 하나를 선택하는 것이다.

 

13.6 예제 코드

class Breed {
	public:
    	Monster* newMonster() {
        	// Monster에서 Breed가 friend로 선언되었기 때문에 생성자가 private이어도 가능하다.
        	// 팩토리 메서드 패턴의 생성자
            return new Monster(*this); 
        }
    
    	Breed(int health, const char* attack)
        : health_(health), attack_(attack) {}
        
        int getHealth() { return health_; }
        const char* getAttack() { return attack_; }
        
    private:
    	int health_;
        const char* attack_;
};

class Monster {
	friend class Breed;

	public:
        const char* getAttack() { return breed_.getAttack(); }
        
    private:
    	Monster(Breed& breed)
        : health_(breed.getHealth()), breed_(breed) {}
        
    	int health_;
        Breed& breed_;      
};
// 몬스터를 생성하는 코드
Monster* monster = someBreed.newMonster();

 

Breed 클래스에 생성자 함수 newMonster를 정의하면 Monster 클래스에 초기화 제어권을 넘겨주기 전에 메모리 풀이나 커스텀 힙에서 메모리를 가져올 수 있다. (위 예제의 return new Monster(*this)가 아닌 다른 방법으로 메모리를 얻을 수 있다는 뜻이다.)  몬스터를 생성할 수 있는 유일한 곳인 Breed 클래스 안에 이런 로직을 둠으로써, 모든 몬스터가 정해놓은 메모리 관리 루틴을 따라 생성되도록 강제할 수 있다.

 

타입 객체 클래스 간의 상속(프로그래밍 언어의 상속이 아님)으로 데이터 공유하기

Breed 클래스의 멤버로 Breed 객체(부모 Breed)를 추가한다. // Breed* parent_;

하위 객체는 데이터를 상위 객체로부터 받을지, 자기 값으로 오버라이드 할지 제어할 수 있어야 한다.

 

1. 속성 값을 요청받을 때마다 동적으로 위임하는 방식으로 구현

ㄴ 종족 속성 값이 런타임에 바뀔 때 사용 

 

2. 객체 생성 시점에 위임 (copy - down 위임)

ㄴ 종족 속성 값이 바뀌지 않을때 사용

 

13.7 디자인 결정

타입 객체 시스템의 사용자는 프로그래머가 아닌 경우가 많아서 이해하기 쉽게 만들어야 한다.

 

타입 객체를 숨길 것이가? 노출할 것인가?

숨기는 경우

ㄴ 타입 사용 객체는 타입 객체로부터 동작을 선택적으로 오버라이드할 수 있다.

ㄴ 타입 객체 메서드를 전부 포워딩해야 한다.

 

노출하는 경우

ㄴ 타입 사용 클래스 인스턴스를 통하지 않고도 외부에서 타입 객체에 접근할 수 있다.

ㄴ 타입 객체가 공개 API의 일부가 된다.

타입 사용 객체를 어떻게 생성할 것인가?

타입 사용 객체를 생성한 뒤에 타입 객체를 넘겨주는 경우 (Monster* monster = new Monster(someBreed) )

ㄴ 외부 코드에서 메모리 할당을 제어할 수 있다.

 

타입 객체의 '생성자' 함수를 호출하는 경우 (위 예제의 방법, Monster* monster = someBreed.newMonster() )

ㄴ 타입 객체에서 메모리 할당을 제어한다.

타입을 바꿀 수 있는가?

타입을 바꿀 수 없다면 

ㄴ 디버깅 하기 쉽다.

 

타입을 바꿀 수 있다면

ㄴ 객체 생성 횟수가 줄어든다. (바꿀 수 없다면 객체를 지우고 다시 만드는 방식으로 구현해야 하기 때문에)

 

상속을 어떻게 지원할 것인가?

상속 없음, 단일 상속, 다중 상속 중 선택할 수 있다.

상속이 많을수록 데이터 중복을 피할 수 있지만, 이해하기 복잡하고 런타임 낭비가 일어날 수 있다.

+ Recent posts