I'm FanJae.

[C++ Design Pattern] Template Method Pattern 본문

C++/Design Pattern

[C++ Design Pattern] Template Method Pattern

FanJae 2024. 9. 18. 23:07

1. Template Method Pattern의 정의 

오퍼레이션에는 알고리즘의 처리 과정만을 정의하고 각 단계에서 수행할 구체적인 처리는 서브클래스에서 정의한다. Template Method 패턴은 "알고리즘 처리과정은 변경하지 않고 알고리즘 각 단계의 처리를 서브클래스에서 재정의" 할 수 있게 한다.

 

1-1. 예제를 통한 Template Method Pattern의 필요성 이해 (코드의 중복)

#include <iostream>
#include "Painter.h"

class Shape
{
public:
   virtual ~Shape() {}
   virtual void draw() = 0;
};

class Rect : public Shape
{
public:
   void draw() override
   {
       std::cout << "draw rect" << std::endl;
   }
};

class Circle : public Shape
{
public:
    void draw() override
    {
        std::cout << "draw circle" << std::endl;
    }
};

int main()
{
    Shape* s1 = new Rect();
    Shape* s2 = new Circle();
    
    s1->draw();
    s2->draw();
}
// painter.h

#pragma once
#include <iostream>

class PainterPath
{
public:
	void begin() {}
	void end() {}

	void draw_rect() {
		std::cout << "draw rect" << std::endl;
	}
	void draw_circle() {
		std::cout << "draw circle" << std::endl;
	}
};

class Painter
{
public:
	void draw_path(const PainterPath & path) {}
};

- GUI 환경에서 윈도우에 그림을 그린다면, 대부분의 라이브러리에는 그림을 그리기 위한 클래스를 제공할 것임.

- 화면 깜빡임 등을 방지(flicker free)하기 위해 다양한 방법을 제공한다.

- 라이브러리가 이런 형태로 동작하는 것이지 실제로 이 함수가 그림을 그려주는 것은 절대 아니다.

 

PainterPath path;
path.begin();

// path 객체를 사용해서
// 원하는 그림을 그린다.

path.end();

Painter surface;
surface.draw_path(path);

 

#include <iostream>
#include "Painter.h"

class Shape
{
public:
   virtual ~Shape() {}
   virtual void draw() = 
};

class Rect : public Shape
{
public:
   void draw() override
   {
        PainterPath path;
        path.begin();

        // path 멤버 함수로 그림을 그린다.
        path.draw_rect();

        path.end();

        Painter surface;
        surface.draw_path(path);
   }
};

class Circle : public Shape
{
public:
    void draw() override
    {
        PainterPath path;
        path.begin();

        // path 멤버 함수로 그림을 그린다.
        path.draw_circle();

        path.end();

        Painter surface;
        surface.draw_path(path);
    }
};

int main()
{
    Shape* s1 = new Rect();
    Shape* s2 = new Circle();
    
    s1->draw();
    s2->draw();
}

- 이와 같이 만들 수 있다. 다만, 문제점이 있다면 이 코드의 문제는 코드의 중복이 너무 많다.

- 잘 보면, 모든 도형을 그리는 함수가 거의 일치하다.

 

1-2. 중복 제거

PainterPath path;
path.begin();

// path 멤버 함수로 그림을 그린다.
// path.draw_rect();

path.end();

Painter surface;
surface.draw_path(path);

- 중복이 되는 부분은 위와 같다. 이를 기반 클래스인 Shape의 draw()에서 제공하면 모든 도형이 물려받을 수 있다.

 

#include <iostream>
#include "Painter.h"

class Shape
{
public:
   virtual ~Shape() {}
   virtual void draw()
   {
        PainterPath path;
        path.begin();

        // path 객체를 사용해서 원하는 그림을 그린다.

        path.end();

        Painter surface;
        surface.draw_path(path);   
   }
};

- 이 부분을 자세히 보면 전체적인 구조 자체(알고리즘의 처리 과정)은 변하지 않는다.

- 이 부분은 파생 클래스 (도형)마다 변경되어야 한다.

 

① 핵심

- 변하지 않은 코드 내부에 변해야 하는 코드를 찾는다.

- 변해야 하는 코드는 가상함수로 분리한다.

- 파생 클래스는 알고리즘 처리 과정을 물려 받으면서 가상함수 재정의를 통해서 변경이 필요한 부분만 다시 만들 수 있다.

 

② 반영 방법

#include <iostream>
#include "Painter.h"

class Shape
{
public:
   virtual ~Shape() {}
   virtual void draw()
   {
        PainterPath path;
        path.begin();

        // path 객체를 사용해서 원하는 그림을 그린다.
        draw_imp(path);
        path.end();

        Painter surface;
        surface.draw_path(path);   
   }
   virtual void draw_imp(PainterPath& path) = 0;
};

- 이와 같이 변하지 않는 부분(전체적인 알고리즘)은 물려 받는다.

- 변해야 되는 부분(그림을 그리는 부분)만 최소화 해서 정의 한다.

 

③ 코드의 수정

#include <iostream>
#include "Painter.h"

class Shape
{
public:
   virtual ~Shape() {}
   virtual void draw()
   {
        PainterPath path;
        path.begin();

        // path 객체를 사용해서 원하는 그림을 그린다.
        draw_imp(path);
        path.end();

        Painter surface;
        surface.draw_path(path);   
   }
   
   virtual void draw_imp(PainterPath& path) = 0;
};

class Rect : public Shape
{
public:
    void draw_imp(PainterPath& path) override
    {
         path.draw_rect();
    }
};

class Circle : public Shape
{
public:
    void draw_imp(PainterPath& path) override
    {
         path.draw_circle();
    }
};

int main()
{
    Shape* s1 = new Rect();
    Shape* s2 = new Circle();
    
    s1->draw();
    s2->draw();
}

- 이와 같이 줄일 수 있다.

④ 코드의 2차 수정

#include <iostream>
#include "Painter.h"

class Shape
{
public:
   virtual ~Shape() {}
   void draw()
   {
        PainterPath path;
        path.begin();

        // path 객체를 사용해서 원하는 그림을 그린다.
        draw_imp(path);
        path.end();

        Painter surface;
        surface.draw_path(path);   
   }
protected:  
   virtual void draw_imp(PainterPath& path) = 0;
};

class Rect : public Shape
{
public:
    void draw_imp(PainterPath& path) override
    {
         path.draw_rect();
    }
};

class Circle : public Shape
{
public:
    void draw_imp(PainterPath& path) override
    {
         path.draw_circle();
    }
};

int main()
{
    Shape* s1 = new Rect();
    Shape* s2 = new Circle();
    
    s1->draw();
    s2->draw();
}

① draw_imp()의 protected 처리

- 사용자 관점에서 보면 draw_imp()는 호출할 일이 없다. draw()만 사용하기 때문에 말이다.

- 따라서, 멤버 함수 내부적으로만 호출이 가능하도록 protected()로 해주는 것이 좋다.

- 외부에서 직접 부를일이 없기 때문이다.

② draw()의 가상함수 처리 삭제

- draw()는 파생 클래스에서 재정의 될 일이 없다.

- 따라서, draw()를 가상함수로 둘 이유가 없다.

 

2. 정리

class Shape
{
public:
    void draw()
    {
       ~~~
       ~~~
       ~~~
    }
    virtual method1() = 0;
    virtual method2() = 0;
};

- 알고리즘의 기본적인 처리 과정은 변경되지 않는다. 즉, 전체적인 틀(Template)은 변화가 없다.

- 하지만 파생 클래스에 따라서 변화가 필요한 부분(변경할 기회를 주고 싶은 코드)는 가상함수로 분리한다.

 

 

 

'C++ > Design Pattern' 카테고리의 다른 글

[C++ Design Pattern] Strategy Pattern  (0) 2024.09.22
Comments