본문 바로가기
Engineer/물리엔진

[감 익혀보기] Strategy Design Pattern with C++

by _제이빈_ 2022. 3. 31.

본문을 공부하며 정리한 내용입니다.

 

 

프로그래밍을 하다보면 변경은 꼭 일어나기 때문에 항상 대비를 해둬야한다. 너무 딱딱하게 작성하면 안되고 유연해야한다. 그러기 위해 디자인 패턴을 사용하는 것이고, 디자인 패턴은 프로그램을 유연하고 독립적이고 유지관리가 편하고 사용성 높도록 도와준다.

 

그 중 Strategy 패턴은 계속 달라지는 행위를 정의할 때 클래스를 수정하지 않아도 되게끔 해준다. 몇 가지 원칙을 보면서 본 디자인 패턴을 익혀보자


Principle 1. 변하는 것은 항상 캡슐화 하자!

항상 클래스가 어떻게 행동(behavior)하는지를 살피고, 찾았다면 이를 캡슐화 해야한다. 예를 들어서 오리 클래스를 생각해보자면 울음과 비행이 행동(behavior)이 되겠다:

 

 

class Duck{
    void quack();
    void fly();
};

class KingDuck:public Duck{
};

 

그러면 하위 클래스를 만들때마다 울음과 비행 방법을 새로 정의를 해줘야하는데, 이와같이 상속해주는 방법으로는 이게 쉽지가 않다. 이런 경우 울음과 비행, 즉 발견해낸 행동(behavior)을 캡슐화해서 클래스 밖으로 꺼내주는 거다. 이렇게 하면 클래스에 직접 행동(behavior)을 구현하는게 아니라 인터페이스만 만들어 놓고 행위 클래스를 호출만 해주게 된다.


Principle 2. 인터페이스를 만들어라! 구현말고!

즉, 슈퍼클래스로 코드를 만들라는 것이다! 아래 예시에서 2번을 사용하라는 말이다.

 

// 1 - Implementation
Beagle *myBeagle=new Beagle(); //instance of beagle
Pittbull *myPittbull=new Pittbull(); //instance of pittbull

// 2 - Interface : super-class
Dog *myBeagle=new Beagle(); //instance of beagle
Dog *myPittbull=new Pittbull(); //instance of pittbull

 

명령은 다 Dog에서 내리되, Beagle 클래스에서 행위들이 정의될 것이다. 일단 여기서 잘 이해가 안가겠지만(저도 잘 안가요 사실) 오리 예시로 계속 설명이 이어지니 그 예시로 모두 이해가 될 것이다!

 

Principle 3. 확장에는 오픈! 수정에는 클로즈!

이미 짜여진 코드에 대해서는 수정이 불가능하고, 확장만 가능해야한다. 이런 유연성은 Strategy 패턴이 가능하도록 도와준다. 지입중!!

 


위에 소개된 3가지 원칙이 그렇게 와닿지 않을탠데, 이제 찐하게 이해해 보자!

 

오리는 울음과 비행이라는 행위를 가지지만, 어떤 오리는 불가능할 것이다. 각자 다른 행동 카테고리들이 있으니 이를 캡슐화해야한다. 고유의 클래스를 만들지만 오리 클래스와의 인터페이스를 만들어 줄 것이다.

 

그런데 인터페이스가 뭔가?

 

인터페이스는 어떤 클래스가 방법만 정의되어있고 명확한 구현은 해 놓지 않은 상태를 의미한다. 게다가 생성자도 포함하고 있지 않다. 즉 이 인터페이스 클래스는 인스턴스화가 되질 않는다. 그냥 행동의 껍데기만 정의해둔 것이다.

 

그러면 인터페이스는 어떻게 만드는가?

 

C++에서는 인터페이스라는 문법이 없고, 가상(Virtual) 함수가 존재한다. 가상 함수는 매서드를 선언만 해놓고 0으로 초기화 해주는게 전부이다.

 

 class MyVirtualClass{
  public:
      virtual void talk()=0;
};

오오오오리이이이이

자 그러면 위에서 다뤄봤던 오리의 행위들을 캡슐화하는 방법을 알아보자. 울음(quack)과 비행(fly)가 있었다. 일단 비행에 대해서만 다루도록하고, 이를 C++의 가상함수로 정의하면 아래와 같다.

 

// Duck.h
class FlyBehavior {
public:
    virtual void fly() = 0;
};

 

바로 이 FlyBehavior 클래스가 인터페이스가 된다. 이 인터페이스를 상속받아 진짜 행위들을 정의한다음, 이 인터페이스를 통해 오리 클래스의 행위들에 연결해주는 것이다!

 

자 이제 껍데기는 만들어 뒀으니 진짜 어떻게 울고 비행하는지를 구현할 필요가 있다. 상속 클래스를 만들어서 실제 행위를 구현해 보자.

 

// Duck.h
class TenSecFly :public FlyBehavior {
private:

public:
    TenSecFly() {};
    ~TenSecFly() {};

    void fly();
};

class CanNotFly :public FlyBehavior {
private:

public:
    CanNotFly() {};
    ~CanNotFly() {};

    void fly();
};

 

그러면 이제 fly의 구현부도 있어야한다! 작성해주자

 

// Duck.cpp
#pragma once
#include "Duck.h"
#include <iostream>
using namespace std;

void TenSecFly::fly() {
	cout << "Only Ten sec I can fly" << endl;
}


void CanNotFly::fly() {
	cout << "I cannot fly" << endl;
}

 

 

이제 이 비행 클래스를 행동으로 설정하고 불러내는 수정된 클래스를 보자

 

// Duck.h
class DuckClass {

protected: // Can Used Only in its subclasses

    FlyBehavior *flyBehavior = 0;
    // QuackBehavior* quackBehavior;  // Not used in this example;

public:
    DuckClass() {};
    ~DuckClass() {};

    void performFly();
    
    // void performQuack();  // Not used in this example;

    void setFlyBehavior(FlyBehavior *uFlyBehavior);

    //setQuackBehavior(QuackBehavior* uQuackBehavior);  // Not used in this example;
};

 

performFly와 setFlyBehavior도 구현해 주자! 오리클래스에 아까 만들어둔 TenSecFly 등과 같은 클래스들의 껍데기, 즉 진짜 행위들을 인터페이스 클래스인 FlyBehavior를 통해 주고 받는 연결해준다. 이 부분도 구현하면 다음과 같다.

 

void DuckClass::performFly(){

    flyBehavior->fly(); //FlyBehavior fly() method gets called. It prints if the duck can fly or not.

}

void DuckClass::setFlyBehavior(FlyBehavior *uFlyBehavior){

    flyBehavior=uFlyBehavior; //set the fly behavior
}

 

그러면 이제 DuckClass의 하위 클래스로 진짜 특정 오리들을 만들어 낼 수 있다. Mallard라는 오리 종을 정의하려면 아래와 같이 상속 받으면 된다.

 

class MallardDuckClass:public DuckClass{

private:

public:

    MallardDuckClass(){
        flyBehavior=new TenSecFly();

    };

    ~MallardDuckClass(){};

};

 

일단 생성자에서 "십초간 날 수 있다"고 초기화했는데, 만약 Mallard 오리의 비행 특성이 있다면 FlyBehavior를 상속받아 새로운 비행 방법을 저장해주고 setFlyBehavior로 연결해주면되는 것이다. 일단 여기서는 비행특성을 새로정의하지는 않았으며, CanNotFly로 연결해 주었다! 예시는 아래와 같다!

 

// main.cpp
#include "Duck.h"

int main()
{
    //1. I create a mallard duck class
    //Notice how I code to the interface, not the implementation
    DuckClass* mallard = new MallardDuckClass();

    //3. performFly prints that the mallard duck does fly
    mallard->performFly();

    //4. Now I'm going to create a new behaviors which takes away the quacking and flying from the mallard duck

    FlyBehavior* nowICantFly = new CanNotFly();

    //5. I'm going to set the new quack behaviors to the mallard duck
    mallard->setFlyBehavior(nowICantFly);

    //8. performFly prints that the mallard duck does NOT fly
    mallard->performFly();


	return 0;
}

 

 

여기서 소스코드를 볼 수 있습니다.

 

반응형

댓글