템플릿 패턴 Template Pattern, Template Callback Pattern

템플릿 패턴 Template Pattern, 템플릿 콜백 패턴 Template Callback Pattern

  • 템플릿 패턴 : 알고리즘의 구조를 서브 클래스가 확장할 수 있도록 템플릿으로 제공하는 방법.
  • 템플릿 콜백 패턴 : 상속 대신 위임을 사용하는 템플릿 패턴.
  • 템플릿 코드를 재사용하고 중복 코드를 줄일 수 있다.
  • 구체적인 알고리즘만 변경할 수 있다.

구현

  • 아래는 템플릿 콜백 패턴이다.
  • 클라이언트 개발자가 구현하기 바라는 메서드를 Operator 인터페이스로 정의하였다.
public interface Operator {
    int apply(int result, int i);
}
  • Processor은 템플릿이다.
  • 주입된 int[] 배열에 대하여 하나로 합치기(reduce)를 기대하는 템플릿이다. 로직을 Operator로 주입한다.
public class Processor {
    private int[] ints;

    public Processor(int[] ints) {
        this.ints = ints;
    }

    public int process(int init, Operator operator) {
        int result = init;
        for (int i : ints) {
            result = operator.apply(result, i);
        }
        return result;
    }
}
  • Operation을 클라이언트 개발자는 입력된 모든 값을 곱하거나 더하는 것으로 정의하였다.
int[] ints = {1,2,3,4};
Processor processor = new Processor(ints);
processor.process(1, (i1, i2) -> i1 * i2); // 24
processor.process(0, Integer::sum); // 10

리스코프 치환 원칙 Liskov substitution principle

  • 템플릿 패턴은 클라이언트 개발자에게 상속 혹은 구현을 미룬다.
  • 다만, 클라이언트 개발자는 상위 클래스가 기대하는 로직을 잘못 이해하여 잘못 코드를 작성할 수 있다. 이 경우 템플릿은 기대하는 방향대로 동작하지 않을 수 있다.
  • 리스코프 치환이 잘 수행된 코드는 다음과 같은 특징을 가진다.
    • 하위 클래스가 상위 클래스를 대체할 수 있어야 한다. 하위 클래스가 상위 클래스를 상속(extends)하거나 구현(implements)한다.
    • 하위 클래스가 상위 클래스보다 접근 제한이 좁지 않다.
  • 위의 원칙을 어긴 코드를 자바는 컴파일 하지 못한다. 왜냐하면 상위 클래스가 기대하는 방향대로 하위 클래스가 동작하지 않기 때문이다.
  • 이를 더 확장한다면, 컴파일을 성공하더라도 작성한 하위 클래스가 기대하는 방향대로 동작하지 않으면 리스코프 치환 원칙을 어긴 것과 같다. 우리는 템플릿의 남은 부분을 클라이언트 개발자가 기대하지 않은 방향으로 작성할 수 있음을 상정해야 한다.

  • 아래 코드는 주입된 ints 배열을 합치지 않고 의미 없는 코드를 작성하였다. 컴파일은 성공하였지만 리스코프 치환 원칙을 어겼다고 볼 수 있다.
int[] ints = {1,2,3,4};
Processor processor = new Processor(ints);
processor.process(1, (i1, i2) -> i1 = i2); // 의미 없는 코드가 되어버린다.

템플릿 패턴과 할리우드 원칙

  • 할리우드 원칙이란 남이 호출하기 전까지 동작하지 않는 원칙을 말한다.
  • 저수준의 객체는 고수준의 객체가 호출하기 전까지 단독으로 동작하지 않는 원칙을 의미한다.
  • 클라이언트 개발자가 작성한 알고리즘은 고수준의 템플릿이 호출되기 전까지 결코 동작하지 않는다.
  • 고수준이 저수준의 객체를 호출하는 명확하고 단순한 흐름을 가진다.
Processor processor = new Processor(ints);
// 구현된 Operator는 Processor가 호출하지 않을 경우 영원히 동작하지 않는다.
processor.process(1, (i1, i2) -> i1 * i2); 

훅 hook

  • 훅은 클라이언트가 필요하다고 판단할 경우 override 하기를 기대하는 메서드이다. 이미 구현된 코드이다.
  • 템플릿 메인 로직이 특정 부분은 상황에 따라 동작하거나 혹은 동작하지 않기를 기대할 수 있다. 템플릿을 상속한 객체가 재작성하여 흐름을 조작할 수 있다.
public int process(int init, Operator operator) {
    int result = init;
    for (int i : ints) {
        int temp = operator.apply(result, i);
        if(print()){
            System.out.printf("fn(%d, %d)=%d\n", result, i, temp);
        }
        result = temp;
    }
    
    return result;
}

public boolean print(){
    return true;
}
  • 위의 코드에는 if(print()) 절이 훅이다.
  • 예제에서 print()는 언제나 true를 반환한다. 그리고 반복문이 끝날 때마다 콘솔에 중간 보고를 한다.
  • 하위 클래스는 해당 로직이 필요없다고 판단할 수 있다. 이 경우 @Override boolean print(){return false}로 재작성하여 더 이상 중간 보고를 하지 않을 수 있다.
  • 훅은 템플릿 패턴을 유연하게 만든다.

참조

  • 백기선, 디자인 패턴 강의(https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4)
  • O’Reilly, “헤드 퍼스트 디자인 패턴”