헤드퍼스트 디자인 패턴 책에는 오리를 이용하여 스트래티지 패턴을 설명하였습니다. 스트래티지 패턴은 OOP의 중요한 원칙 중 하나인 “상속보다는 합성” 원칙을 직접 표현하고 있기 때문에 반복학습을 통한 확실한 이해를 하기 위해 오리가 아닌 액션어드벤쳐 게임을 주제로 삼아 이 패턴을 다시한번 설명하도록 하겠습니다.

전체 클래스와 인터페이스를 한눈에 보기 위해 starUML을 이용해 클래스 다이어그램을 작성해 보았습니다.

Strategy Pattern - Action Adventure Game

Strategy Pattern - Action Adventure Game

먼저 Character 클래스는 Knight 와 Wizard 클래스의 추상층입니다. 각 직업들이 가지는 공통적인 특성을 정의하여 서브클래스에서 확장(상속) 사용하도록 합니다.

모든 캐릭터들은 무기를 사용할 수 있고 갑옷을 입을 수 있기 때문에 이 두가지의 기능을 커다란 알고리즘으로 분류하여 각각 인터페이스를 만듭니다.
무기를 사용하는 것은 useWeapon() 메서드를 호출하여 무기를 사용하는 모습을 그래픽으로 처리하게 하고, 갑옷을 착용하면 갑옷 종류에 따라 다른 보너스 hp 를 얻도록 할 계획입니다.

아래의 IArmor 인터페이스를 보면 wearBonus() 메서드의 리턴값이 int 로 되어 있음을 볼 수 있습니다. 이것은 갑옷 종류에 따른 hp 보너스 포인트를 리턴할 수 있도록 한 것입니다.

0
1
2
3
4
5
6
package
{
	public interface IArmor
	{
		function wearBonus():int;
	}
}

IArmor 인터페이스를 구상하는 클래스중 하나인 Plate 클래스를 보면 인터페이스에 있는 메서드를 구현하고 있고 wearBonus() 메서드에서 int 값 50을 리턴하고 있습니다.

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package
{
	public class Plate implements IArmor
	{
		public function Plate ()
		{
			trace( "판금갑옷을 착용하였습니다." )
		}
 
		public function wearBonus():int
		{
			trace( "현재 hp에 + 50" )
			return 50;
		}
	}
}

이런식으로 아무것도 입지 않은 Naked 클래스를 포함하여, 필요한 갑옷마다 hp 보너스를 설정하여 인터페이스를 구현합니다.

그럼 이제 IWeapon 과 IArmor 인터페이스를 사용하게 되는 Character 클래스를 보면 모든 캐릭터들이 공통적으로 필요한 변수를 설정하고 몇가지에는 getter 를 설정하였습니다. 각 메서드에 대한 설명은 주석으로 달아놓았습니다.

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package
{
	public class Character
	{
		protected var weapon:IWeapon;
		protected var armor:IArmor;
		protected var _hp:int = 0;
		protected var _name:String;
		protected var _characterClass:String;
 
		public function Character ()
		{
			trace( "-----새로운 캐릭터를 생성합니다.-----" )
		}
 
		//무기를 장비함
		public function equipWeapon( $weapon:IWeapon ):void
		{
			weapon = $weapon;
		}
 
		//무기를 사용함
		public function weaponAction():void
		{
			weapon.useWeapon()
		}
 
		//갑옷을 입음
		public function equipArmor( $armor:IArmor ):void
		{
			armor = $armor;
 
			//해당 갑옷의 보너스 hp를 리턴받아 캐릭터의 hp에 더함
			_hp += armor.wearBonus();
 
			//방어력, 이동속도 등 다른 착용효과 구현 가능
		}
 
		//현재 장비하고 있는 무기를 리턴
		public function getObjectWeapon():IWeapon
		{
			return weapon
		}
 
		//캐릭터 이름, 직업, hp 현황을 리턴
		public function getStat():String
		{
			return _name + "(" + _characterClass + ") 현재 hp : " + _hp
		}
 
		public function get hp():int { return _hp; }
 
		public function get name():String { return _name; }
 
		public function get characterClass():String { return _characterClass; }
	}
}

그리고 Character 클래스를 확장한 Knight 클래스에서는 Character 클래스에서 protected 로 선언하여 상속한 변수들에 초기값을 입력하여 캐릭터를 생성하게 됩니다.

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package
{
	public class Knight extends Character
	{
		public function Knight ( $name:String )
		{
			_hp = 100;
			_name = $name;
			_characterClass = "기사"
			trace( name , " (이)라는 이름을 가진 ", characterClass ," 캐릭터를 생성하였습니다." )
			weapon = new Fist();
			armor = new Naked();
		}
	}
}

여기까지 하면 주먹( new Fist(); ) 이라는 무기와 아무것도 걸치지 않은 맨몸( new Naked(); )의 hp 100짜리 기사 캐릭터가 생성 됩니다.

IArmor를 짚어나가고 있으므로 그 부분을 집중해서 보도록 하겠는데요, 여기서 중요한 부분은 Character 클래스에서 IArmor 데이터형으로 인스턴스 변수를 생성한 armor 에[01] new Naked(); 라는 갑옷를 대입하여 현재 갑옷상태를 아무것도 입지 않은 상태로 초기 지정하였습니다. 즉, 추상층[02] 에서는 상위 형(type)인 인터페이스 형(type)으로 변수 선언을 하고, 구상층에서 상속받은 그 변수에 구체적인 형태를 대입하는 것입니다. 이렇게 하는 이유는 아래 호스트 코드에서 살펴보도록 하겠습니다.

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package
{
	import flash.display.Sprite;
 
	public class Main extends Sprite
	{
		public function Main():void
		{
			var player1:Character = new Knight( "세계의끝" );
							//출력 : -----새로운 캐릭터를 생성합니다.-----
							//출력 : 세계의끝  (이)라는 이름을 가진  기사  캐릭터를 생성하였습니다.
							//출력 : 맨주먹입니다. (무기가 없습니다)
							//출력 : 아무것도 착용하지 않았습니다.
			trace( player1.getStat() );		//출력 : 세계의끝(기사) 현재 hp : 100
			player1.weaponAction();		//출력 : 주먹을 휘두릅니다.
			player1.equipWeapon( new Sword() );	//출력 : Sword 를 장비하였습니다.
			player1.weaponAction();		//출력 : Sword 를 휘두릅니다
			player1.equipArmor( new Plate() );	//출력 : 판금갑옷을 착용하였습니다.
							//출력 : 현재 hp에 + 50
			trace( player1.getStat() );		//출력 : 세계의끝(기사) 현재 hp : 150
			trace( player1.getObjectWeapon() );	//출력 : [object Sword]
 
			var player2:Character = new Wizard( "댄스댄스댄스" );
							//출력 : -----새로운 캐릭터를 생성합니다.-----
							//출력 : 댄스댄스댄스  (이)라는 이름을 가진  마법사  캐릭터를 생성하였습니다.
							//출력 : 맨주먹입니다. (무기가 없습니다)
							//출력 : 아무것도 착용하지 않았습니다.
			trace( player2.getStat() );		//출력 : 댄스댄스댄스(마법사) 현재 hp : 80
			player2.weaponAction();		//출력 : 주먹을 휘두릅니다.
			player2.equipWeapon( new Staff() );	//출력 : Staff 를 장비하였습니다.
			player2.weaponAction();		//출력 : Staff 에서 마법을 발사합니다.
			player2.equipArmor( new Leather() );	//출력 : 가죽갑옷을 착용하였습니다.
							//출력 : 현재 hp에 + 30
			trace( player2.getStat() );		//출력 : 댄스댄스댄스(마법사) 현재 hp : 110
			trace( player2.getObjectWeapon() );	//출력 : [object Staff]
		}
	}
}

8 번 라인을 보면, 위에서 언급한 것과 마찬가지로 new Knight( “세계의끝” ) 를 상위 type 인
var player1:Character 로 받고 있는데요, 이런 형태의 캐스팅을 하게 되면, 인스턴스 변수를 사용할 때에 그게 어떤 구체적인 형태인지 호스트코드에서 알 필요 없이 알아서 사용됩니다.
위의 player1은 type 이 Character 지만 Knight 클래스라는 것을 player1 은 이미 알고 있다는 의미 입니다. 마찬가지로 Character 클래스의 equipArmor() 메서드에서 armor.wearBonus() 를 하면 캐릭터가 뭘 입었는 몰라도,[03] 보너스 hp는 조사하면 다 나온다는 것이죠.

이러한 구조는 아래와 같은 절차지향 코드의 조건문을 상쇄한 효과를 냅니다.

0
1
2
3
4
5
6
7
8
9
10
function wearBonus():int {
	var bonus:int;
	if ( armor == "Plate" ) {
		bonus = 50;
	} else if ( armor == "Leather" ) {
		bonus = 30;
	} else {
		bonus = 0;
	}
	return bonus;
}

게다가 새로운 갑옷이 늘어날 때마다 else if 를 추가하지 않아도 됩니다. 무엇보다 확장에 유연하고 수정사항이 생겼을때 어디를 얼마만큼 고쳐야 하는지 명확하게 알 수 있게 됩니다.

zelda_link_to_the_past_stamp

Strategy Pattern : Action Adventure Game 액션스크립트 코드 다운로드 (93)


[출처 : http://ufx.kr/blog/163]

+ Recent posts