템플릿 패턴 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, “헤드 퍼스트 디자인 패턴”