본문 바로가기
일상,취미

직접 구현 하면서 이해해보는 전략패턴

by 진득한진드기 2024. 4. 14.

취미로 게임 개발을 하면서 게임이 극한의 OOP라는 말이 있어서 전 부터 미뤄왔던 OOP에 대해서 공부하려고 헤드퍼스트디자인패턴이란 책을 E-Book으로 사서 보고 있다.

 

여기서 1장 내용이 디자인 패턴의 세계로 들어가보자는 내용인데

대략적인 객체지향 프로그래밍의 상속, 다형성, 오버로딩, 오버라이딩만 알고 있어도 쉽게 따라갈 수 있게 만든 좋은 책인 것 같다.

 

먼저 이 책은 JAVA로 기술되어 있지만, 나는 JAVA를 학교에서 배운게 다 이기에..... 그냥 평소에 알고리즘을 풀던 C++로 구현하였다.

그래서 구현상 오점이 있을수도 있으니 틀렸다면 댓글에 남겨주시길 바랍니다....

1장은 전략패턴을 기술 해놓았는데, 예시는 다음과 같다.

 

오리(Duck)이라는 부모 클래스를 만들면, 소리를 낸다 던지(quack), 수영을 하는 메서드(swim)가 존재할 것이다. 그리고 이를 확인하는 display()라는 메서드가 존재할 것이다.

 

부모 클래스(super)

class Duck {
public:
    virtual quack(){}
    virtual swim() {}
    virtual display() {}
};

 

 

자식 클래스

 

class MallarDuck : public Duck {
    void display() override {

    }
};
class RedheadDuck : public Duck {
    void display() override {
        
    }
};

 

 

만약에 여기서 오리가 나는 기능을 넣는다고 하면, fly() 메서드를 추가할 수 있다.

 

class Duck {
public:
    virtual quack(){}
    virtual swim() {}
    virtual display() {}
    virtual fly() {}
};

 

그러면 모든 서브 클래스가 fly()까지 상속 받는데 만약에 장난감 오리나, 러버덕 같은 홍보용 오리들은 날 수 없다.

 

날지 말아야 할 오리들이 나는 문제가 발생 할 수 있고, 사용하지 않는다면 크게 문제는 없지만 유지보수 측면에서 좋지 않다.

 

만약에 규격이 계속 바껴 상황을 보고 코드를 자주 수정해줘야할 상황이면, 위와 같은 행동은 좋은 코드라고 보기 힘들어진다.

 

한 가지 행동을 바꿀 때마다 그 행동이 정의되어 있는 서로 다른 서브 클래스를 다 찾아서 일일히 고치기는 힘들고 버그가 생길 확률이 높아진다.

 

이런 상황에 딱 맞는 디자인 원칙이 전략패턴이다.

 

디자인의 첫 원칙은 애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리하는 것이다.

 

이 원칙은 

 

"바뀌는 부분은 따로 뽑아서 캡슐화한다. 그러면 나중에 바뀌지 않는 부분에는 영향을 미치지 않고 그 부분만 고치거나 확장할 수 있다."로 확장하여 생각이 가능하다.

 

자 바뀌는 부분과 그렇지 않은 부분을 분리하고 생각을 해보자.

 

위에서 본 예로는 fly()와 quack()을 제외하면 Duck 클래스는 문제 없다.

 

나머지 부분은 fly()와 quack()만큼 자주 변경되지 않는다.

 

변화하는 부분과 그대로 있는 부분을 분리하려면 2개의 클래스 집합을 만들어야 한다.

 

각 클래스 집합에는 각각의 행동을 구현한 것을 전부 집어 넣는다.

 

기존 관계도

 

기존에 있던 클래스간 관계도가 이와 같다면, 변화하는 행동을 분리한 관계도는 다음과 같다.

 

행동에 따른 분리 관계도

 

특정에 행동에 따른 클래스들을 파생하여 만든다.

 

class FlyBehavoir {
public:
    virtual void fly() = 0;
};
class FlyWithWings : public FlyBehavoir{
public:
    void fly() override {
        cout << "날개가 있어서 야무지게 날기\n";
    }
};
class FlyNoway : public FlyBehavoir {
public:
    void fly() override {
        cout << "날개가 없어서 못날아\n";
    }
};
class FlyRocketPowerd : public FlyBehavoir {
public:
    void fly() override {
        cout << "로켓 추진으로 날아갑니다.\n";
    }
};
class QuackBehaivor {
public:
    virtual void quack() = 0;
};
class Quack : public QuackBehaivor {
public:
    void quack() override {
        cout << "꽥꽥\n";
    }
};
class Squack : public QuackBehaivor {
public:
    void quack() override {
        cout << "고무 오리 ~~\n";
    }
};
class MuteQuack : public QuackBehaivor {
public:
    void quack() override {
        cout << "입 다물어\n";
    }
};

 

코드로는 위와 같이 표현할 수 있다.

 

Duck 클래스에 적용시키기 위해 Duck클래스를 아래와 같이 변경한다.

 

class Duck{
public:
    QuackBehaivor* quackbehavior;
    FlyBehavoir* flybehavior;
    virtual void swim(){
        cout << "오리 수영하기\n";
    }
    virtual void display() {
        cout << "오리 보기\n";
    }
    void performQuack() {
        quackbehavior->quack();
    }
    void performFly() {
        flybehavior->fly();
    }
    void setFlyBehavior(FlyBehavoir *fb){
        flybehavior = fb;
    }
    void setFlyBehavior(QuackBehaivor *qb){
        quackbehavior = qb;
    }
};

 

Duck 클래스 내부에 특정행동들에 상황에 맞춰 동적으로 변환이 가능한 변수를 두고 상황에 맞게 동작하게 한다.

 

테스트 해보자.

 

int main(){
    Duck* dk = new MallarDuck();
    dk->performQuack();
    dk->performFly();
    delete dk;

    return 0;
}

 

 

 

결과가 아래와 같이 나온다.

 

결국 모든 오리들은 Duck 클래스를 확장해서 만들고, 자주 변화하는 행동을은 캡슐화하여 생각하면 되는 것이다.

 

여기에 행동으로 생각하는 대신 알고리즘군으로 생각하면 전략패턴으로 모델링 하는 것이다.

 

전체 관계도

 

 

각 오리에는 FlyBehavior와 QuackBehavior가 있다. 나는 행동과 우는 행동을 위임 받는다.

 

이런 식으로 두 클래스를 합치는 것을 '구성(composition)이용한다' 라고 부른다. 여기서는 오리 클래스에서 행동을 상속 받는 대신, 올바른 행동 객체로 구성되어 행동을 부여받는다.

 

이런 구성은 디자인 원칙에 세번째 원칙이다.

 

여기서 알아가는 객체지향의 원칙은 3가지이다.

 

1. 바뀌는 부분은 캡슐화한다.

2. 상속보다는 구성을 활용한다.

3. 구현보다는 인터페이스에 맞춰서 프로그래밍한다.

 

오리와 같은 상황을 만들어 한번 더 활용해보자.

 

어떠한 RPG게임을 한다고 가정 했을때 직업이 있고, 각자 들 수 있는 무기가 있다고 할 때,

 

부모 클래스로 Character가 있고 자식 클래스로 Queen, King, Knight, Troll 이 있다 표현할 수 있다.

 

부모 클래스 Character

class Character {
public:
    virtual void fight() {}
};

 

 

자식 클래스

 

class Queen: public Character {
public:
    void fight() override {
        cout << "Queen Attacks ";
    }
};
class King: public Character {
public:
    void fight() override {
        cout << "King Attacks ";
    }
};
class Troll: public Character {
public:
    void fight() override {
        cout << "Troll Attacks ";
    }
};
class Knight: public Character {
public:
    void fight() override {
        cout << "Knight Attacks ";
    }
};

 

여기서 캐릭터마다 다른 무기를 사용할 수 있고, 그 무기들 마다 공격하는 기능이 다르다면 Character 클래스에 WeaponBehavior를 붙여 다음과 같이 나타낼 수 있다.

 

무기에 따른 공격방식 분리

class WeaponBehavior{
public:
    virtual void useWeapon() = 0;
};
class SwordBehavior : public WeaponBehavior {
    void useWeapon() override {
        cout << "with Sword\n";
    }
};
class BowAndArrow : public WeaponBehavior {
    void useWeapon() override {
        cout << "with Arrow\n";
    }
};
class Knife : public WeaponBehavior {
    void useWeapon() override {
        cout << "with Knife\n";
    }
};
class AxeBehavior : public WeaponBehavior {
    void useWeapon() override {
        cout << "with Axe\n";
    }
};

 

Character에는 우리가 정리한대로 WeaponBehavior 행동을 담을 수 있는 변수를 추가한다.

 

class Character {
public:
    virtual void fight() {}
    void setWeapon(WeaponBehavior* weaponbehavior){
        myweapon = weaponbehavior;
    }
    void weaponAttack() {
    	myweapon->useWeapon();
    }
protected:
    WeaponBehavior* myweapon;
};

 

 

적용해서 간단하게 테스트해보자.

 

int main() {
    Character *character = new Knight();
    WeaponBehavior* now_weapon = new Knife();
    character->setWeapon(now_weapon);
    character->fight();
    character->weaponAttack();
    
    delete character;
    delete now_weapon;
    return 0;
}

 

 

잘 나오는 것이 확인된다.

 

관계도로 보면 아래와 같이 표현할 수 있다.

 

 

 

진짜 게임 돌리는것 처럼 테스트해보자.

 

게임 클래스를 추가하여 

class Game {
public:
    Character* makeCharacter(int number) {
        switch (number)
        {
        case 1:
            cout << "여왕을 선택하셨습니다.\n";
            return new Queen();
        case 2:
            cout << "기사를 선택하셨습니다.\n";
            return new Knight();
        case 3:
            cout << "왕을 선택하셨습니다.\n";
            return new King();
        case 4:
            cout << "트롤을 선택하셨습니다.\n";
            return new Troll();
        default:
            return new Character();
        }
    }
    WeaponBehavior* makeWeapon(int number){
        switch(number){
        case 1:
            cout << "검을 고르셨습니다\n";
            return new SwordBehavior();
            break;
        case 2:
            cout << "활을 고르셨습니다\n";
            return new BowAndArrow();
        case 3:
            cout << "칼을 고르셨습니다\n";
            return new Knife();
        case 4:
            cout << "도끼를 고르셨습니다\n";
            return new AxeBehavior();
        default:
            return new SwordBehavior();
        }
    }
    template<class T>
    void playgame(T *character,WeaponBehavior *weapon){
        character->setWeapon(weapon);
        character->fight();
        character->weaponAttack();
    }
};

 

그냥 시뮬레이션 되는 대로 바로 구현한거라 비효율적으로 보일수도 있다.

그냥 너그럽게 이해부탁....ㅋㅋㅋ ㅠ

 

먼저 캐릭터를 생성하고 그에 맞는 무기를 선택하고 공격했을 때를 보여주는 테스트 코드이다.

 

위와 같이 Game 클래스를 생성하고 마지막으로 playgame()을 실행시켜 테스트해보자.

 

int main(){
    Game game;
    int character,weapon;
    cout << "====================\t\t게임이 시작되었습니다 어떤 캐릭터를 생성하시겠습니까\t\t=========================\n";
    cout << "====================\t\t1.Queen\t2.Knight\t3.King\t4.Troll\t\t==============================\n";
    cin >> character;
    Character* myCharacter = game.makeCharacter(character);   
    cout << "==========================\t\t무기를 고르시오\t\t========================================\t\t\n";
    cout << "====================\t\t1.Sword\t2.Arrow\t3.Knife\t4.Axe\t\t==============================\n";
    cin >> weapon;
    WeaponBehavior* myWeapon = game.makeWeapon(weapon);
    game.playgame(myCharacter,myWeapon);
    
    delete myCharacter;
    delete myWeapon;

    return 0;
}

 

결과물을 봐보자. 

 

 

아주 잘 나오는 것을 알 수 있다.

 

아직 실력이 부족해 코드를 바로바로 깔끔짜지는 못하지만 디자인 패턴에 익숙해지면 좀 더 깔끔하게 짤 수 있을 것 같다는 느낌이 든다.

 

따로따로 클래스 별로 보면 헷갈릴 수 있으니 전체 코드는 다음과 같다.

 

#include <iostream>

using namespace std;

class WeaponBehavior{
public:
    virtual void useWeapon() = 0;
};
class SwordBehavior : public WeaponBehavior {
    void useWeapon() override {
        cout << "with Sword\n";
    }
};
class BowAndArrow : public WeaponBehavior {
    void useWeapon() override {
        cout << "with Arrow\n";
    }
};
class Knife : public WeaponBehavior {
    void useWeapon() override {
        cout << "with Knife\n";
    }
};
class AxeBehavior : public WeaponBehavior {
    void useWeapon() override {
        cout << "with Axe\n";
    }
};
class Character {
public:
    Character() : change_weaponCount(0){}
    virtual void fight() {}
    // 심심해서 넣어본 무기 제한
    void setWeapon(WeaponBehavior* weaponbehavior){
        if(change_weaponCount > 10){
            cout << "너무 많이 무기를 바꿔서 종료하겠습니다\n";
            return;
        }
        ++change_weaponCount;
        myweapon = weaponbehavior;
    }
    void checking_count(){
        cout << change_weaponCount << '\n';
    }
    void weaponAttack(){
        myweapon->useWeapon();
    }
protected:
    WeaponBehavior* myweapon;
private:
    int change_weaponCount = 0;
};
class Queen: public Character {
public:
    void fight() override {
        cout << "Queen Attacks ";
    }
};
class King: public Character {
public:
    void fight() override {
        cout << "King Attacks ";
    }
};
class Troll: public Character {
public:
    void fight() override {
        cout << "Troll Attacks ";
    }
};
class Knight: public Character {
public:
    void fight() override {
        cout << "Knight Attacks ";
    }
};
class Game {
public:
    Character* makeCharacter(int number) {
        switch (number)
        {
        case 1:
            cout << "여왕을 선택하셨습니다.\n";
            return new Queen();
        case 2:
            cout << "기사를 선택하셨습니다.\n";
            return new Knight();
        case 3:
            cout << "왕을 선택하셨습니다.\n";
            return new King();
        case 4:
            cout << "트롤을 선택하셨습니다.\n";
            return new Troll();
        default:
            return new Character();
        }
    }
    WeaponBehavior* makeWeapon(int number){
        switch(number){
        case 1:
            cout << "검을 고르셨습니다\n";
            return new SwordBehavior();
            break;
        case 2:
            cout << "활을 고르셨습니다\n";
            return new BowAndArrow();
        case 3:
            cout << "칼을 고르셨습니다\n";
            return new Knife();
        case 4:
            cout << "도끼를 고르셨습니다\n";
            return new AxeBehavior();
        default:
            return new SwordBehavior();
        }
    }
    template<class T>
    void playgame(T *character,WeaponBehavior *weapon){
        character->setWeapon(weapon);
        character->fight();
        character->weaponAttack();
    }
};
int main(){
    Game game;
    int character,weapon;
    cout << "====================\t\t게임이 시작되었습니다 어떤 캐릭터를 생성하시겠습니까\t\t=========================\n";
    cout << "====================\t\t1.Queen\t2.Knight\t3.King\t4.Troll\t\t==============================\n";
    cin >> character;
    Character* myCharacter = game.makeCharacter(character);   
    cout << "==========================\t\t무기를 고르시오\t\t========================================\t\t\n";
    cout << "====================\t\t1.Sword\t2.Arrow\t3.Knife\t4.Axe\t\t==============================\n";
    cin >> weapon;
    WeaponBehavior* myWeapon = game.makeWeapon(weapon);
    game.playgame(myCharacter,myWeapon);
    
    delete myCharacter;
    delete myWeapon;

    return 0;
}

 

좀 더 나아가면 주석 부분 처럼 무기 변경을 제한 할 수도 있다.

 

뭐..... 특정 시간안에 너무 많이 바꾸는 것을 제한을 둔다던지 하여 조건을 줄 수 있을 것 같다.

 

이렇게 간단하게 전략패턴을 알아보았다.

 

내가 생각하는 전략패턴은 상황에 따른 알고리즘을 생각하여 수정이 잦은 알고리즘군을 추상화하여 행동들을 상황에 맞게 사용할 수 있게 해주는 패턴으로 파악 됐다.

 

좀 더 깊이 이해하면 더 얻어갈 수 있을 것 같지만 사전적 정의는 다음과 같다.

 

전략 패턴은 결국에 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해준다.

클라이언트로부터 알고리즘을 분리해서 독립적으로 변경 할 수 있다.

 

다음엔 옵저버 패턴에 대해 알아보겠다.

'일상,취미' 카테고리의 다른 글

다시 시작해보는 TIL....1일차  (0) 2024.06.04
옵저버 패턴의 쓰임새  (1) 2024.04.21
Somethink 리팩토링 현황  (0) 2024.02.27
최근 나의 동향 및 생각  (1) 2024.02.07
첫 블로그 시작!  (0) 2022.09.14