C++로 전략 패턴 구현하기

Kang Chang Hwan·2024년 5월 21일

CS - Design-Pattern

목록 보기
1/6

Interface로 설계해보는 건 어떨까?

interface로 오리마다 달라지는 부분( fly( ), quack( ) )을 만들고 Duck의 subtype이 이를 implement하게 되면 아래 코드와 같이 변경할 수 있었다.

void main() {

}

abstract class Duck {
  void swim() {
    print("Duck is swimming");
  }

  void display();
}
// fly(), quack()을 interface로 만듦.
abstract interface class Flyable {
  void fly();
}

abstract interface class Quackable {
  void quack();
}

class MallardDuck extends Duck implements Flyable, Quackable{
  
  void display() {
    print('looks like a mallard');
  }

  void fly() {
    print("mallardDuck is flying!!");
  }

  void quack() {
    print("mallardDuck is quack");
  }
}

class RedheadDuck extends Duck implements Flyable, Quackable {
  
  void display() {
    print('looks like a redhead');
  }

  void fly() {
    print("redheadDuck is flying!!");
  }

  void quack() {
    print("redheadDuck is quack");
  }
}

class RubberDuck extends Duck implements Quackable{
  
  void quack() {
    print("RubberDuck Squeak");
  }

  
  void display() {
    print("looks like a rubberDuck");
  }
}

class DecoyDucks extends Duck {
  
  void display() {
    print("looks like decoy Duck");
  }
}

그리고 책의 질문은 다음과 같다.

이러한 디자인은 어떻게 생각하냐?

직접 디자인해서 구현해본 결과 느낀 점은 다음과 같다.

  1. override를 하게 되면 결국 구현 사항은 자식 마다 구현해야 되니 코드를 재사용 못하는 건 똑같은데, interface를 사용하게 되면 가질 수 있는 장점은 fly( ), quack( ) 등 Duck의 subtype에서는 해당 행동을 하지 않을 수도 있기 때문에(바뀌는 부분) 이에 대해서 대응할 수 있다는 장점이 있었다.

  2. 단점은 인터페이스는 method의 body가 없기 때문에 이를 implement하는 모든 Duck의 subtype의 method의 body를 구현해줘야 한다. > 이게 얼마나 큰 단점인지는 체감이 안됐다.

Duck의 subtype이 48개라고 하고 fly( )와 quack( )의 구현사항이 같다고 가정하고 이를 인터페이스화 해서 자식마다 구현했다고 치자. 근데 fly( )의 동작을 조금 바꾸려면 어떻게 해야 할까?
48개의 자식들의 fly 구현을 전부 일일이 바꿔줘야 한다는 엄청난 trade-off가 따른다.

이럴 땐 어떻게 할 수 있을까?

문제 상황

  1. 상속은 코드를 재사용할 순 있지만 subtype별로 다른 행동에 대해서 일일이 대응하기가 까다로웠다.
  2. Interface는 subtype별로 다른 행동에 대해서 일일이 대응할 순 있지만 행동에 변화가 필요하면 subtype별로 일일이 바꿔줘야 한다는 단점(코드 재사용 X)이 있다.

일단 확실한 건, 부모 class에서 파생될 때 변경되는 부분과 변경되지 않는 부분이 존재한다는 것이었고 변경되는 부분을 대응하기 위해서(핵심 문제) 어떻게 해야 할지 고민하는 것이다.

즉, 코드 재사용도 살리되(상속) subtype별로 대응(interface)을 해야 하는게 솔루션이니...

아래에서는 부모 class는 놔두되 (왜냐하면 변경되지 않는 부분이 있으니까) subtype별로 대응도 해야 하니(변경되는 부분) interface를 사용하되 이 부분을 재활용할 수 있으면 되니까 이를 implement한 class를 재활용하는 approach를 소개하고 있다.

Zeroing in on the problem... (문제를 명확히 파악하기!)

DESIGN PRINCIPLE

어플리케이션에서 변화하는 부분(A)을 찾아서 변화하지 않는 부분(B)과 분리해 캡슐화한다! 후에 A가 변경되어도 A이외의 부분에 영향을 미치지 않도록 한다!

여기서 소개한 Approach는 다음과 같다.

  1. Duck의 Behavior의 interface 모음을 만든다.(변하는 것들 따로 pull out)
  2. Duck class는 그대로 둔다. swim( )은 변하지 않기 때문에 재사용할 가치가 있음.
  3. 해당 interface를 구현한 각 subtype에 맞는 behavior class를 만든다.
  4. Duck에 Behavior Type의 포인터와 동적 바인딩을 위한 가상 함수를 만든다. 즉, upcasting을 활용해 부모 타입에서는 자식의 구현물을 몰라도 실행할 수 있게함.
  5. Duck을 상속한 subtype에서 Behavior interface type의 포인터에 객체의 주소를 할당한다. 상속받은 가상함수의 body를 구현한다.
  6. SuperType의 포인터에 subtype의 객체 주소를 할당하고
  7. runtime에 해당 포인터가 객체의 주소를 참조하여 구현되어 있는 함수를 실행한다.
#include <iostream>
using namespace std;

class FlyBehavior {
    public:
      virtual void fly() = 0;
      virtual ~FlyBehavior() {}
};

class QuackBehavior {
  public:
    virtual void quack() = 0;
    virtual ~QuackBehavior() {}
};

class Duck {
  protected:
    FlyBehavior* flyBehavior;
    QuackBehavior* quackBehavior;
  public:
    void swim () {
      cout << "Duck is Swimming!!" << endl;
    }
    virtual void display () = 0;
    virtual void performFly() {
      flyBehavior->fly();
    }
    virtual void performQuack() {
      quackBehavior->quack();
    }
    virtual ~Duck() {
      delete flyBehavior;
      delete quackBehavior;
    }
};

class FlyWithWings : public FlyBehavior {
  public:
    void fly() override {
        cout << "Duck is Flying!!!" << endl;
    }
};

class Quack : public QuackBehavior {
  public:
    void quack() override {
        cout << "꽥" << endl;
    }
};

class MallardDuck : public Duck {
public:
  MallardDuck() {
    flyBehavior = new FlyWithWings();
    quackBehavior = new Quack();
  }
  void display () override {
    cout << "MallardDuck !!!" << endl;
  }
};

class FlyNoWay : public FlyBehavior {
    public:
      void fly() override {
        cout << "날 수 없는 덕.." << endl;
      }
};


class Squeak : public QuackBehavior{
  public:
    void quack() override {
        cout << "삑삑" << endl;
    }
};


class MuteQuack : public QuackBehavior{
  public:
    void quack() override {
        cout << "" << endl;
    }
};

int main () {
  Duck* mallardDuck = new MallardDuck();
  mallardDuck->performFly();
  mallardDuck->performQuack();
  delete mallardDuck;
  return 0;
}

사실 핵심은 다형성이라는 개념을 활용하기 위해 upcasting과 동적 바인딩을 알아야 된다고 생각해서 dart언어로 실습하며 이해하다가 c++을 통해 더 low level의 원리를 파악하기 위해 c++로 실습을 진행했다.

main 함수만 설명하자면

컴파일시 mallardDuck이라는 변수 이름과 주소, type 이 symbolTable에 add되는데 그 MallardDuck 객체의 주소에 접근하면 Local SymbolTable이 형성되고 객체의 프로퍼티와 메서드가 삽입되어있다.

여기서 주목할 점은 virtual pointer인데(줄여서 vptr) vptr에는 해당 객체 인스턴스의 vtable 주소가 있고 이를 참조해보면 가상 함수 이름과 주소가 할당되어 있다.

그래서 컴파일 때는 Global symbol table에 위처럼 삽입되지만 런타임때는

mallardDuck의 주소인 0xx2에 접근하고 vptr의 주소인 0xxxx1에 접근해 performFly와 performQuack을 실행할 수 있는 것이다. 즉, 실제 구현 사항을 실행하는 것이다.

이렇게 실행되는 것을 볼 수 있다.

추가로 virtual function과 late binding, late object memory alloc의 개념이 필요해 학습 후 포스팅 예정이다.

Strategy Pattern

Q. 해당 패턴이라는 이름으로 어떤 내용이 전달되는가?

  • 변하는 부분과 변하지 않는 부분을 분리하고 변하지 않는 부분이 변하는 부분의 인터페이스를 참조한다. > 변하는 부분을 묶어 클래스들의 집합으로 캡슐화되어 있음.
  • subtype에서는 해당 인터페이스를 구현한 클래스를 할당하고 변하는 부분에 대해서는 구현체에게 위임한다.
  • 위임은 Upcasting과 Vtable의 Vfuntion을 통해 이뤄진다.

핵심 내용

Interface/Supertype에 맞춰서 programming 하기

다음 예시를 보자.

Dog d = new Dog();
d.bark();

이 경우에는 변수 d는 Dog type으로 선언(declaring)됐다. 즉, d는 구체적인 구현에 맞춰서 코딩해야 함.

이 경우는 어떨까?

Animal d = new Dog();
d.makeSound();

변수 d는 Animal type으로 선언됐다. d.makeSound( )를 호출하면 compile-time에는 Symbol Table에 Animal type의 포인터인 d에 Dog( )의 메모리 주소가 할당되고 run-time에는 해당 주소에 접근해 vPointer가 가리키는 vTable의 Dog::bark()함수의 주소에 접근해 실제 dog의 bark 메서드를 호출하게 된다.

이렇게 subtype의 할당하는 것을 하드코딩하는 것 대신에 다음 방법도 활용할 수 있다.

a = getAnimal( );
a.makeSound();

런타임에 동적으로 Animal Type의 객체를 할당받는 것이다.

그림으로 정리


Strategy Pattern

정리하자면 위 그림과 같다.

Strategy pattern은 함수(행동)는 인터페이스의 함수로 똑같은데 concrete class의

profile
아쉬움 없이 살자. 모든 순간을 100%로!

0개의 댓글