헤드퍼스트 디자인 패턴 책에는 오리를 이용하여 스트래티지 패턴을 설명하였습니다. 스트래티지 패턴은 OOP의 중요한 원칙 중 하나인 “상속보다는 합성” 원칙을 직접 표현하고 있기 때문에 반복학습을 통한 확실한 이해를 하기 위해 오리가 아닌 액션어드벤쳐 게임을 주제로 삼아 이 패턴을 다시한번 설명하도록 하겠습니다.
전체 클래스와 인터페이스를 한눈에 보기 위해 starUML을 이용해 클래스 다이어그램을 작성해 보았습니다.
먼저 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 를 추가하지 않아도 됩니다. 무엇보다 확장에 유연하고 수정사항이 생겼을때 어디를 얼마만큼 고쳐야 하는지 명확하게 알 수 있게 됩니다.
Strategy Pattern : Action Adventure Game 액션스크립트 코드 다운로드 (93)