💡 비슷한 코드를 실행하는 if-else 블록은 전략 패턴 적용 대상이다.
public class Calculator {
public int calculate(boolean firstGuest, List<Item> items) {
int sum = 0;
for (Item item: items) {
if (firstGuest)
sum += (int)(item.getPrice() * 0.9);
else if (!item.isFresh())
sum += (int)(item.getPrice() * 0.8);
else
sum += item.getPrice();
}
return sum;
}
}
// 전략 패턴을 적용한 Calculator의 구현
public class Calculator {
private DiscountStrategy discountStrategy;
public Calculator(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}
public int calculate(List<Item> items) {
int sum = 0;
for (Item item: items) {
sum += discountStrategy.getDiscountPrice(item);
}
return sum;
}
}
// Calculator에서 사용하는 전략 인터페이스
public interface DiscountStrategy {
int getDiscountPrice(Item item);
}
// DiscountStrategy 인터페이스를 구현한 콘크리트 클래스
public class FirstGuestDiscountStrategy implements DiscountStrategy {
@Override
public int getDiscountPrice(Item item) {
return (int)(item.getPrice() * 0.9);
}
}
public class Client {
private DiscountStrategy strategy;
public void onFirstGuestButtonClick() {
// 첫 손님 할인 버튼 누를 때 생성
strategy = new FirstGuestDiscountStrategy();
}
public void onCalculationButtonClick() {
// 계산 버튼 누를 때 실행
Calculator cal = new Calculator(strategy);
int price = cal.calculate(items);
}
}
public class Client {
private DiscountStrategy strategy;
public void onFirstGuestButtonClick() {
strategy = new FirstGuestDiscountStrategy();
}
public void onLastGuestButtonClick() {
// 마지막 손님 대폭 할인 버튼 누를 때 생성
strategy = new LastGuestDiscountStrategy();
}
public void onCalculationButtonClick() {
Calculator cal = new Calculator(strategy);
int price = cal.calculate(items);
}
}
💡 실행 과정/단계는 동일한데 각 단계 중 일부의 구현이 다른 경우에 사용할 수 있는 패턴
템플릿 메서드 패턴의 구성
상위 클래스: 실행 과정을 구현한 메서드 제공
[기존 코드]
DB/LDAP에서 사용자 정보를 가져오는 부분의 구현만 다르고, 인증을 처리하는 과정은 완전히 동일
public class DbAuthenticator {
public Auth authenticate(String id, String pw) {
// 사용자 정보로 인증 확인
User user = userDao.selectById(id);
boolean auth = user.equalPassword(pw);
// 인증 실패시 익셉션 발생
if (!auth)
throw createException();
// 인증 성공시, 인증 정보 제공
return new Auth(id, user.getName());
}
private AuthException createException() {
return new AuthException();
}
}
public class DbAuthenticator {
public Auth authenticate(String id, String pw) {
// 사용자 정보로 인증 확인
User user = userDao.selectById(id);
boolean auth = user.equalPassword(pw);
// 인증 실패시 익셉션 발생
if (!auth)
throw createException();
// 인증 성공시, 인증 정보 제공
return new Auth(id, user.getName());
}
private AuthException createException() {
return new AuthException();
}
}
[템플릿 메서드를 사용해 리팩토링]
코드 중복 문제를 제거하면서, 동시에 코드를 재사용 가능하게 만들어준다.
템플릿 메서드
public abstract class Authenticator {
// 템플릿 메서드
public Auth authenticate(String id, String pw) {
if (!doAuthenticate(id, pw))
throw createException();
return createAuth(id);
}
protected abstract boolean doAuthenticate(String id, String pw);
private RuntimeException createException() {
throw new AuthException();
}
protected abstract Auth createAuth(String id);
}
Authenticator 클래스를 상속받는 하위클래스는 authenticate() 메서드에서 호출하는 메서드만 알맞게 재정의해주면 된다.
public class LdapAuthenticator extends Authenticator {
@Override
protected boolean doAuthenticate(String id, String pw) {
return ldapClient.authenticate(id, pw);
}
@Override
protected Auth createAuth(String id) {
LdapContext ctx = ldapClient.find(id);
return new Auth(id, ctx.getAttributes("name"));
}
}
일반적인 경우, 하위타입이 흐름을 제어한다.
즉, 하위 타입이 상위 타입의 기능을 재사용할지 여부를 결정한다
// 일반적인 경우
// turnOn 메서드는 상위 클래스의 turnOn 메서드 재사용 여부를 자신이 결정
public class SuperCar extends ZetEngine {
@Override
public void turnOn() {
// 하위 클래스에서 흐름 제어
if (notReady)
beep();
else
super.turnOn();
}
}
반면, 템플릿 메서드 패턴에선 상위 타입의 템플릿 메서드가 모든 실행 흐름을 제어함.
하위 타입의 메서드는 템플릿 메서드에서 호출되는 구조
위의 예시에선 템플릿 메서드에서 호출하는 메서드를 추상 메서드로 정의했는데, 기본 구현을 제공하고 하위 클래스에서 알맞게 재정의하도록 구현할 수도 있음: 훅(hook) 메서드
이 경우 해당 메서드는 기능의 확장 지점으로 사용됨.