헤드퍼스트 디자인패턴
- 본 책을 읽고 책의 내용을 간략하게 정리한 글입니다.
Chapter 8. 알고리즘 캡슐화하기 - 템플릿 메서드 패턴
예제 프로젝트 - 커피와 홍차
- 커피와 홍차의 공통점은 카페인이 있다는 점과 만드는 방법이 매우 비슷한 방법으로 만들어진다는 점이다.
Coffee 클래스와 Tea 클래스 만들기
- 다음은 커피를 만드는 클래스이다.
public class Coffee {
void prepareRecipe() {
boilWater();
brewCoffeeGrinds();
pourInCup();
addSugarAndMilk();
}
public void boilWater() {
System.out.println("물 끓이는 중");
}
public void brewCoffeeGrinds() {
System.out.println("필터로 커피 우려내는 중");
}
public void pourInCup() {
System.out.println("컵에 따르는 중");
}
public void addSugarAndMilk() {
System.out.println("설탕과 우유를 추가하는 중");
}
}
- 그리고 다음은 홍차를 만드는 클래스이다.
public class Tea {
void prepareRecipe() {
boilWater();
steepTeaBag();
addLemon();
pourInCup();
}
public void boilWater() {
System.out.println("물 끓이는 중");
}
public void steepTeaBag() {
System.out.println("티백을 우려내는 중");
}
public void addLemon() {
System.out.println("레몬을 추가하는 중");
}
public void pourInCup() {
System.out.println("컵에 따르는 중");
}
}
커피와 홍차 클래스 추상화하기
prepareRecipe() 메서드 추상화
- 커피나 홍차를 우려내는 일은 거의 같다.
- 설탕이나 레몬 등 첨가물을 넣는 일 또한 거의 같다.
void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
CaffeineBeverage 추상화 슈퍼클래스
public abstract class CaffeineBeverage {
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
// Coffee와 Tea에서 서로 다르게 처리하므로 추상 메서드로 선언
// 서브클래스가 알아서 처리하도록 둠
abstract void brew();
abstract void addCondiments();
void boilWater() {
System.out.println("물 끓이는 중");
}
void pourInCup() {
System.out.println("컵에 따르는 중");
}
}
커피와 홍차 서브클래스
public class Coffee extends CaffeineBeverage {
public void brew() {
System.out.println("필터로 커피 우리는 중");
}
public void addCondiments() {
System.out.println("설탕과 우유를 추가하는 중");
}
}
public class Tea extends CaffeineBeverage {
public void brew() {
System.out.println("티백을 우리는 중");
}
public void addCondiments() {
System.out.println("레몬을 추가하는 중");
}
}
템플릿 메서드 패턴 알아보기
- 위 CaffeineBeverage 클래스에서의
prepareRecipe()
메서드가 바로 템플릿 메서드이다.- 어떤 알고리즘의 템플릿(틀) 역할을 한다.
- 템플릿 내에서 알고리즘의 각 단계는 메서드로 표현된다.
- 어떤 메서드는 클래스 내에서 처리되기도 하고, 어떤 메서드는 서브클래스에서 처리되기도 한다.
- 서브클래스에서 처리해야 하는 메서드는
abstract
로 선언해야 한다.
- 서브클래스에서 처리해야 하는 메서드는
템플릿 메서드 패턴의 장점
- 알고리즘이 한 군데에 모여 있으므로 수정이 필요할 때 한 부분만 수정하면 된다.
- 다른 서브클래스도 쉽게 추가할 수 있는 구조를 제공한다.
템플릿 메서드 패턴의 정의
- 템플릿 메서드 패턴은 알고리즘의 골격을 정의한다.
- 템플릿 메서드 패턴을 사용하면 알고리즘의 일부 단계를 서브클래스에서 구현할 수 있으며, 알고리즘의 구조는 그대로 유지하면서 알고리즘의 특정 단계를 서브클래스에서 재정의할 수도 있습니다.
- 템플릿이란, 일련의 단계로 알고리즘을 정의한 메서드이다.
템플릿 메서드 속 후크
- 후크는 추상 클래스에서 선언되지만 기본적인 내용만 구현되어 있거나 아무 코드도 들어있지 않은 메서드이다.
- 서브클래스는 다양한 위치에서 알고리즘에 끼어들 수 있다.
public abstract class CaffeineBeverageWithHook {
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
if (customerWantsCondiments()) {
addCondiments();
}
}
abstract void brew();
abstract void addCondiments();
void boilWater();
void pourInCup();
// 별 내용 없이 return true만 하는 메서드
// 이 메서드는 서브클래스에서 필요할 때 오버라이드할 수 있는 메서드로, 후크이다.
boolean customerWantsCondiments() {
return true;
}
}
후크 활용하기
public class CoffeeWithHook extends CaffeineBeverageWithHook {
public void brew() {
System.out.println("필터로 커피를 우려내는 중");
}
public void addCondiments() {
System.out.println("우유와 설탕을 추가하는 중");
}
// 첨가물을 넣을지 말지 고객에게 물어본 후 입력한 내용에 따라 true/false 리턴
public boolean customerWantsCondiments() {
String answer = getUserInput();
if (answer.toLowerCase().startsWith("y")) {
return true;
} else {
return false;
}
}
private String getUserInput() {
String answer = null;
System.out.print("커피에 우유와 설탕을 추가할까요? (y/n)");
BufferedReader in = new BufferReader(new InputStreamReader(System.in));
try {
answer = in.readLine();
} catch (IOException e) {
System.err.println("IO 에러");
}
if (answer == null) {
return "no";
}
return answer;
}
}
후크 코드 테스트
public class BeverageTestDrive {
public static void main(String[] args) {
TeaWithHook teaHook = new TeaWithHook();
CoffeeWithHook coffeeWithHook = new CoffeeWithHook();
System.out.println("\n홍차 준비 중..");
teaHook.prepareRecipe();
System.out.println("\n커피 준비 중..");
coffeeWithHook.prepareRecipe();
}
}
할리우드 원칙
디자인 원칙 7
- 먼저 연락하지 마라. 우리가 연락 드리겠다.
- 저수준 구성 요소가 시스템에 접속할 수는 있지만 언제, 어떻게 그 구성 요소를 사용할지는 고수준 구성 요소가 결정한다.
- 즉 고수준 구성 요소가 저수준 구성 요소에게 먼저 연락하지 말라고 얘기하는 것과 같다.
- 할리우드 원칙을 활용하면 의존성 부패(dependency rot)를 방지할 수 있다.
- 의존성 부패 : 어떤 고수준 구성 요소가 저수준 구성 요소에 의존하고, 그 저수준 구성 요소는 다시 고수준 구성 요소에 의존하는 식으로 의존성이 복잡하게 꼬여있는 상황
- 템플릿 메서드 패턴으로 디자인하면 서브클래스에게 우리가 먼저 연락할테니 연락하지 말라고 하는 것과 같다.
자바 API 속 템플릿 메서드 패턴
Arrays
클래스의 정렬
public static void sort(Object[] a) {
Object aux[] = (Object[]) a.clone();
mergeSort(aux, a, 0, a.length, 0);
}
// 템플릿 메서드
private static void mergeSort(Object src[], Object dest[], int low, int high, int off) {
// 많은 코드
for (int i = low; i < high; i++) {
// 템플릿 메서드를 완성하려면 compareTo() 메서드를 구현해야 한다.
for (int j = i; j > low && ((Comparable)dest[j - 1]).compareTo((Comparable)dest[j]) > 0; j--) {
// Arrays 클래스에 이미 정의되어 있는 구상 메서드
swap(dest, j, j - 1);
}
}
// 많은 코드
}
AbstractList
로 나만의 리스트 구현- ArrayList, LinkedList 같은 자바의 리스트 컬렉션은 리스트에서 필요한 기능을 구현해주는 AbstractList 클래스를 확장한다.
- AbstractList에는
get()
과size()
추상 메서드에 의존하는subList()
템플릿 메서드가 있다. - 다음은 String 객체만 담을 수 있는 리스트를 구현했다.
public class MyStringList extends AbstractList<String> {
private String[] myList;
MyStringList(String[] strings) {
myList = strings;
}
public String get(int index) {
return myList[index];
}
public int size() {
return myList.length;
}
public String set(int index, String item) {
String oldString = myList[index];
myList[index] = item;
return oldString;
}
}