삽질 주도 개발

GoF Behavioral Pattern에 해당하는 Template Method Pattern에 대해서 알아보자.

GoF 책에서는 다음과 같이 패턴의 의도를 밝힌다.

객체의 연산에는 알고리즘의 뼈대만을 정의하고 각 단계에서 수행할 구체적 처리는 서브클래스 쪽으로 미룹니다.
알고리즘의 구조 자체는 그대로 놔둔 채 알고리즘 각 단계 처리를 서브클래스에서 재정의할 수 있게 합니다.

 

예시 코드

위의 코드를 보면 커피를 만드는 절차 그리고 차를 만드는 절차를 구현하고 있다.

커피나 차를 만드는 절차 중 boil과  pour, add는 똑같이 필요한 절차이다.

 

그렇다면 그 외에 행위들은 마실 것에 따라 행위가 달라지는데, 절차(뼈대)는 수퍼 클래스에서 추상화하고, 이러한 행위를 서브클래스에서 구체화하는 것이다.

 

그렇다면 상위 클래스부터 공통된 절차를 추상 클래스로 정의해보자.

public abstract class CaffeineBeverage {

    // Template method
    public final void prepareRecipe() {
        boilWater();
        brew();     // 서브클래스에서 구체화
        pourInCup();
        addCondiments();	// 서브클래스에서 구체화
    }

    abstract protected void brew();
    abstract protected void addCondiments();

    private void boilWater() {
        System.out.println("Boiling water");
    }

    private void pourInCup() {
        System.out.println("Pouring into cup");
    }
}

다음과 같이 절차를 정의해놓고, 마실 것(커피/차)에 따라 달라지는 행위는 자식 클래스에서 구현할 수 있도록 abstract로 설정했다.

여기서 절차를 정의해놓은 메서드 혹은 추상 메서드들을 사용하여 알고리즘을 정의하는 메서드를 템플릿 메서드라고 한다.

 

그리고 서브 클래스에서 위 메서드를 구현해보자.

public class Coffee extends CaffeineBeverage {

    @Override
    protected void brew() {
        System.out.println("Dripping Coffee through filter");
    }

    @Override
    protected void addCondiments() {
        System.out.println("Adding Sugar and Milk");
    }

}
public class Tea extends CaffeineBeverage {

    @Override
    protected void brew() {
        System.out.println("Steeping the tea");
    }

    @Override
    protected void addCondiments() {
        System.out.println("Add Lemon");
    }
}

위와 같이 서브클래스에서 구체적인 행위를 정의하여 template method pattern을 만들어 보았다.

여기서 추가적인 기법과 template method를 사용할 때 주의해야 할 점들을 소개하고 마친다.

 

hook method

서브 클래스를 구현할 때 융통성을 발휘하기 위한 메서드이다.

 

기본적인 내용만 구현되어 있거나 내용이 비어있다.

만약 내가 원할 때만 설탕 혹은 레몬을 추가하고 싶을 때 hook method를 사용하여 추가할 수 있다.

package Behavioral.templatemethod.after.beverage;

public abstract class CaffeineBeverage {

    public final void prepareRecipe() {
        boilWater();
        brew();     // 서브클래스에서 구체화
        pourInCup();// 서브클래스에서 구체화

        if(customerWantsCondiments()) {
            addCondiments();
        }
    }

    abstract protected void brew();
    abstract protected void addCondiments();

    private void boilWater() {
        System.out.println("Boiling water");
    }

    private void pourInCup() {
        System.out.println("Pouring into cup");
    }

    // hook method
    private boolean customerWantsCondiments() {
        return true;
    }
}

 

람다를 활용한 Template method pattern

구체화할 추상 메서드가 하나 뿐이라면 람다로 패턴에 적용할 수 있다.

다음과 같이 상점 오픈 전 준비를 한다고 가정한다.

public abstract class Shop {

    public final void prepare() {
        clean();
        check();
    }

    private void clean() {
        System.out.println("Cleaning");
    }

    protected abstract void check();
}

class Supermarket extends Shop {

    @Override
    protected void check() {
        System.out.println("재고 확인");
    }
}

 

이 때 추상 메서드가 하나이기 때문에 다음과 같이 리팩토링할 수 있다.

  • abstract 제거
  • 구현해야할 추상 메서드를 함수형 인터페이스를 인자로 만듦
public class Shop {

    public final void prepare(Runnable doSomething) {
        clean();
        doSomething.run();
    }

    private void clean() {
        System.out.println("Cleaning");
    }
}

 

주의해야할 점

 

  • Template method는 가능한 절제해야 한다.

기반 클래스는 변경될 가능성이 쉽기 때문에 깨지기가 쉽다.

그렇기 때문에 파생 클래스들로 커스터마이징이 많으면 많을수록 변경 점이 더욱 많아진다.

 

  • 추상 메서드의 개수를 최소호화해야 한다.

추상 메서드가 많게 되면 서브 클래스를 구현하기 힘들고 개발자가 해당 메서드를 일일이 파악해야 한다.

 

  • Template method의 불변성이 확실할 때에만 final을 붙여 서브클래스가 오버라이드하는 것을 금지해야 한다.

 


Reference

틀린 점은 지적 부탁드립니다!

'Architecture' 카테고리의 다른 글

GoF Design Pattern - Proxy Pattern  (0) 2022.11.21
GoF Design Pattern - Adapter Pattern  (0) 2022.11.15