결론부터 말하자면 스트래티지 패턴은 객체와, 객체가 가지는 행동들을 따로 분리해 놓는것이다.
이해를 쉽게 하기 위해 롤을 예시로 들어보자.
롤에는 바론, 용, 바위게, 두꺼비등의 여러가지 몬스터 종류가 있다.
각 몬스터들이 가지고있는 특성으로는
1. 챔피언을 공격할 수도 있고, 공격하지 못할 수도 있다.
2. 공격받지 않는 동안 보여주는 애니메이션이 무조건 존재한다 (Idle)
3. 챔피언이 CC기를 사용했을 때, 영향을 받을 수도 있고, 안 받을 수도 있다.
이 세가지 특성만 일단 생각해보고
Monster 클래스를 생각해보자.
abstract Class Monster
{
public abstract void Attack();
public abstract void GotCC();
public void OnIdle() { // 모든 몬스터는 Idle Animation을 가지고 있다 }
}
이를 상속받는 몬스터들은 다음과 같다.
Class Baron : Monster
{
public override Attack() { // 원거리 공격 }
public override GotCC() { } // CC에 영향을 받지 않으므로 빈칸으로 구현한다.
}
Class Dragon : Monster
{
public override Attack() { // 원거리 공격 }
public override GotCC() { } // CC에 영향을 받지 않으므로 빈칸으로 구현한다.
}
Class RockMonster : Monster
{
public override Attack() { } // 공격 할 수 없으므로 빈칸으로 구현한다.
public override GotCC() { // 현재 실드량을 전부 없앤다 }
}
갑자기 이때 기획자가 "에픽 몬스터들이 방어도 할 수 있게 다음 패치 하기로 했어요"라고 말해서
다음 패치까지 수천 수만줄에 다다르는 몬스터 관련 코드들을 바꿔야 한다.
따라서 모든 몬스터 클래스의 부모 클래스인 Monster 클래스를 다음과 같이 바꿔야 겠다고 생각한다.
abstract Class Monster
{
public abstract void Attack();
public abstract void GotCC();
public abstract void Defense(); // 새로운 추상메소드 추가
public void OnIdle();
}
기획에 따르면, 바론과 용만이 방어할 수 있다 했으니 기존에 하던대로
평화로운 바위게는 Defense 메소드를 빈칸으로 구현하면 될것이다.
Class Baron : Monster
{
public override Attack() { // 원거리 공격 }
public override GotCC() { } // CC에 영향을 받지 않으므로 빈칸으로 구현한다.
public override Defense() { // 챔피언을 에어본 시킨다 }
}
Class Dragon : Monster
{
public override Attack() { // 원거리 공격 }
public override GotCC() { } // CC에 영향을 받지 않으므로 빈칸으로 구현한다.
public override Defense() { // 챔피언을 잠시 넉백 시킨다 }
}
Class RockMonster : Monster
{
public override Attack() { } // 공격 할 수 없으므로 빈칸으로 구현한다.
public override GotCC() { // 현재 실드량을 전부 없앤다 }
public override Defense() { } // 방어 할 수 없으므로 빈칸으로 구현한다.
}
겉으로 봤을때는 괜찮아 보이지만 문제점이 있다.
1. 원거리 공격 코드가 재사용이 되지 않는다.
물론 Baron과 Dragon마다 원거리 공격 방식은 살짝살짝 다르지만
공통으로 사용되는 코드가 있을 것이다. 이 코드는 원거리 공격을 하는 몬스터들 마다 그대로 넣어주어야 한다.
2. 정의되는 행동들을 동적으로 바꿀 수 없다.
필요에 의해 바위게도 원거리 공격을 할 수 있게 하려면
미리 바위게의 Attack 메소드에 boolean으로 분기를 해놓은 다음 그때그때마다 행동을 바꾸던지 해야할 것이다.
3. 빈칸으로 구현된 메소드들이 존재한다.
부모 클래스인 Monster 클래스에 있는 Attack, Defense, GotCC 메소드들은
하위 클래스에서 빈칸으로 구현하는 경우가 있다.
물론 빈칸이라 아무것도 실행되지 않지만 가독성도 안좋고 이 몬스터가 어떤 행동을 하는지
한눈에 들어오지 않는다.
이 구조를 좀 더 우아하게 바꿀 수 있는 방법이 있을까?
우선 대부분의 프로그래밍 언어들은 다중상속을 금지하고 있으므로, 인터페이스를 활용하는 방법을 생각해보자
부모 클래스에서 Attack, Defense, GotCC 메소드를 따로 인터페이스로 빼낸다.(영어가 어색해도 이해해주길 바란다 ㅠ)
Class Monster
{
public void OnIdle() { // 모든 몬스터는 Idle Animation을 가지고 있다 }
}
interface IAttackable()
{
void Attack();
}
interface IDefensable()
{
void Defense();
}
interface ICCable()
{
void GotCC();
}
인터페이스로 빼낸다음, 각 몬스터들마다 필요한 기능을 넣어준다.
Class Baron : Monster, IAttackable, IDefensable
{
public void Attack() { // 원거리 공격 }
public void Defense() { // 챔피언을 에어본 시킨다 }
}
Class Dragon : Monster, IAttackable, IDefensable
{
public void Attack() { // 원거리 공격 }
public void Defense() { // 챔피언을 잠시 넉백 시킨다 }
}
Class RockMonster : Monster, ICCable
{
public void GotCC() { // 현재 실드량을 전부 없앤다 }
}
이로써 의미 없는 빈칸 구현을 막았다. 하지만 또 문제점이 존재하는데
1. 여전히 원거리 공격 코드 재사용이 불가능 하다.
인터페이스는 구현을 가지지 못하므로 Baron과 Dragon의 Attack 메소드는 재사용하지 못하게 된다.
이는 나중에 Attack 관련한 리팩토링을 할 때 일일이
다른 Monster들의 Attack메소드들을 수정해줘야 되는 상황을 불러 일으킨다.
2. 여전히 정의되는 행동들을 동적으로 바꿀 수 없다.
동적으로 인터페이스를 추가할 수 없으니 말이다.
이럴때 필요한게 이 포스트에서 설명하는 스트레티지 패턴이다.
스트레티지 패턴의 원칙
클래스에서 달라지는 부분을 찾아 내고, 달라지지 않는 부분으로부터 분리시켜 캡슐화 한다.
이렇게 하면 바뀌지 않는 부분에는 영향을 끼치지 않는 상태로 바뀌는 부분을 고치거나 확장할 수 있다.
우리의 예시에서 바뀌지 않는 부분은 OnIdle부분이라고 할 수 있다.
물론 내부 구현은 몬스터 마다 다를 수 있지만 어쨌든 모든 몬스터는 OnIdle 애니메이션을 가지고 있으니 말이다.
행동들을 더이상 Monster 클래스가 아닌 다른 클래스로 분리시켜보자
Class LongDistanceAttack : IAttackable
{
public void Attack() { //원거리 공격}
}
Class NoAttack : IAttackable
{
public void Attack() { } // 공격하지 않음
}
Class AirborneDefense : IDefensable
{
public void Defense() { // 챔피언을 에어본 시킨다 }
}
Class KnockbackDefense : IDefensable
{
public void Defense() { // 챔피언을 넉백 시킨다 }
}
Class NoCC : ICCable
{
public void GotCC() { } // CC에 영향을 받지 않음
}
Class shieldCC : ICCable
{
public void GotCC() { // CC에 맞으면 실드를 잃는다 }
}
이제 Monster와 행동들은 더 이상 아무런 관계가 없다.
이렇게 분리를 끝내고 원래대로 Monster들에게 행동을 부여하려면 당연히
행동 객체들을 인스턴스로 가지고 있어야 한다.
Class Monster
{
protected IAttackable attack;
protected IDefensable defense;
protected ICcable cc;
protected void OnIdle() { // 모든 몬스터는 Idle Animation을 가지고 있다 }
}
그리고 하위 몬스터들에게는 각 인터페이스 변수에 알맞는 행동 객체들을 설정해 주기 위한 setter 함수도 넣어주자
Class Monster
{
protected IAttackable attack;
protected IDefensable defense;
protected ICcable cc;
public void OnIdle() { // 모든 몬스터는 Idle Animation을 가지고 있다 }
public void SetAttackWay(IAttackable _attack)
=> attack = _attack;
public void SetDefenseWay(IDefensable _defense)
=> defense = _defense;
public void SetCCWay(ICCable _cc)
=> cc = _cc;
}
마지막으로 자신이 가지고 있는 행동을 실행 할 수 있는 함수도 넣어주면 끝이다.
Class Monster
{
protected IAttackable attack;
protected IDefensable defense;
protected ICcable cc;
public void OnIdle() { // 모든 몬스터는 Idle Animation을 가지고 있다 }
public void SetAttackWay(IAttackable _attack)
=> attack = _attack;
public void SetDefenseWay(IDefensable _defense)
=> defense = _defense;
public void SetCCWay(ICCable _cc)
=> cc = _cc;
public void DoAttack()
=> attack.Attack();
public void DoDefense()
=> defense.Defense();
public void GotCC()
=> cc.GotCC();
}
이렇게 하면 위에 써놨던 문제들은 다 해결된다.
1. 여전히 원거리 공격 코드 재사용이 불가능 하다.
원거리 공격을 하는 Baron, Dragon들은 미리 행동이 정의된 LongDistanceAttack을 attack변수로 가지고 있으면 되므로
코드 재사용을 할 수 있다.
Monster baron = new Baron();
baron.SetAttackWay(new LongDistanceAttack());
baron.DoAttack(); // 원거리 공격 실행
Monster dragon = new Dragon();
baron.SetAttackWay(new LongDistanceAttack());
dragon.DoAttack(); // 원거리 공격 실행
2. 여전히 정의되는 행동들을 동적으로 바꿀 수 없다.
동적으로 행동을 변경하거나 추가하는 일은 setter 함수로 손쉽게 할 수 있다.
미리 행동들을 따로 빼내어 정의해두었으니 말이다.
Monster baron = new Baron();
baron.SetDefenseWay(new AirborneDefense());
baron.DoDefense(); // 챔피언을 에어본 시킨다.
baron.SetDefenseWay(new KnockbackDefense());
baron.DoDefense(); // 챔피언을 넉백 시킨다.
스트레티지 패턴은 한마디로 객체와, 객체의 행동을 구현하는 부분을 분리시켜
코드의 재사용을 가능하게 해주고 행동의 동적인 변경이 유연해지도록 해주는 패턴이라고 할 수 있다.
'Programming > 디자인 패턴' 카테고리의 다른 글
[디자인 패턴] 5. 싱글턴 패턴 (0) | 2022.03.22 |
---|---|
[디자인 패턴] 4. 팩토리 패턴 (0) | 2022.03.17 |
[디자인 패턴] 3. 데코레이터 패턴 (0) | 2022.03.17 |
[디자인 패턴] 2. 옵저버 패턴 (0) | 2022.03.10 |
[디자인 패턴] 디자인 패턴이란? (0) | 2022.03.10 |