min, max, minBy, maxBy

위 메서드들은 전부 reduce 특화 함수다.

reduce를 이용해 메서드를 정의한다.

minBy, maxBy 의 경우 배열에 요소에 추가로 적용할 메서드를 인자로 받아 원하는 값으로 비교할 수 있게 해 추상화 수준을 높힌 메서다.

 

/*
 * reduce 특화 함수
 */
static <T> T min(List<T> list, Comparator<T> comparator) {
    return reduce(list, BinaryOperator.minBy(comparator));
}
static <T> T max(List<T> list, Comparator<T> comparator) {
    return reduce(list, BinaryOperator.maxBy(comparator));
}
//주어진 조건으로 검사한 결과로 최대 최소 값을 판단한다.
static <T,R> T minBy(List<T> list,Comparator<R> comparator ,Function<T, R> mapper) {
    return reduce(list, (a,b)-> comparator.compare(mapper.apply(a), mapper.apply(b)) > 0 ? b:a);
}
static <T,R> T maxBy(List<T> list,Comparator<R> comparator ,Function<T, R> mapper) {
    return minBy(list,comparator.reversed(),mapper);
}




/*Stream 클래스 메서드*/
/*
 * reduce 특화 함수
 */
Optional<T> min(Comparator<? super T> comparator) {
    return reduce(BinaryOperator.<T>minBy(comparator));
}
Optional<T> max(Comparator<? super T> comparator) {
    return reduce(BinaryOperator.<T>maxBy(comparator));
}
//주어진 조건으로 검사한 결과로 최대 최소 값을 판단한다.
<R> Optional<T> minBy(Comparator<? super R> comparator ,Function<T, R> mapper) {
    return reduce( (a,b)-> comparator.compare(mapper.apply(a), mapper.apply(b)) > 0 ? b:a);
}
<R> Optional<T> maxBy(Comparator<? super R> comparator ,Function<T, R> mapper) {
    return minBy(comparator.reversed(),mapper);
}
		System.out.println("min, max, minBy, maxBy 테스트");
		System.out.println("나이 "+min(users, (u1,u2)->Integer.compare(u1.age,u2.age)));
		System.out.println("나이 "+max(users, (u1,u2)->Integer.compare(u1.age,u2.age)));
		System.out.println("이름 "+min(users, (u1,u2)-> u1.name.compareTo(u2.name) ));
		System.out.println("이름 "+max(users, (u1,u2)-> u1.name.compareTo(u2.name) ));
		System.out.println("나이 "+minBy(users, Integer::compare, u->u.age));
		System.out.println("나이 "+maxBy(users, Integer::compare, u->u.age));
		System.out.println(
				Stream.stream(users)
					.map(u->u.age)
					.max(Integer::compare));
		System.out.println(
				Stream.stream(users)
				.minBy(Long::compare,u->u.id));
min, max, minBy, maxBy 테스트
나이 [id=80, name=MP, age=23]

나이 [id=10, name=ID, age=36]

이름 [id=20, name=BJ, age=32]

이름 [id=40, name=PJ, age=27]

나이 [id=80, name=MP, age=23]

나이 [id=10, name=ID, age=36]

Optional[36]
Optional[[id=10, name=ID, age=36]
]

정적으로 타입을 체크하는 자바는 모든 경우의 수를 고려해서 내부적으로 max, min을 판단할 수가 없다.

따라서 클라이언트에게 비교기준과 판단을 위임하는 식으로 처리한다.

타입이 명확한 스트림은 실제로 판단기준을 위임하지 않는다.
참조형을 다루는 일반 스트림은 호출자에게 위임한다.

이러한 구조는 폭넓게 해석하면 템플릿 메서드 패턴을 적용했다고 볼 수 있다.

 

다른 방법

static class User implements Comparable<User>{
    final Long id;
    final String name;
    final Integer age;

    public User(Long id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "[id=" + id + ", name=" + name + ", age=" + age + "]\n";
    }

    @Override
    public int compareTo(User o) {
        return Long.compare(this.id, o.id);
    }

}

들어오는 모든 타입 모두 Comparable 인터페이스를 구현하도록 하면 된다.

static <T extends Comparable<T>> T min(List<T> list) {
    return reduce(list, (a,b)->a.compareTo(b)<=0 ? a : b );
}
static <T extends Comparable<T>> T max(List<T> list) {
    return reduce(list, (a,b)->a.compareTo(b)>0 ? a : b );
}

단점으로 정렬 조건을 바꿀 수가 없다. compareTo()를 수정하면 된다고 생각할지 모르지만, 그렇다면 다른 모든 User 클래스 사용하는 곳에 영향을 미친다.

추가로 Stream 클래스에 적용 시 Comparable 구현 여부를 알 수 없어 적용이 힘들다.

위와 같이 제한된 지네릭 클래스로 만들면, 스트림을 생성하려면 반드시 Comparator를 구현해야한다는 제약이 생긴다. Comparator가 필요가 없어도 강제하게 된다.

 

종합해보면 깔끔하게 알고리즘 일부를 외부로 위임하는 템플릿 메서드 패턴을 따르는게 좋다.

전체 코드

package javabasic.stream;

import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.*;
import java.util.stream.IntStream;


public class FuncEx01 {
	static class User implements Comparable<User>{
		final Long id;
		final String name;
		final Integer age;
		
		public User(Long id, String name, Integer age) {
			this.id = id;
			this.name = name;
			this.age = age;
		}

		@Override
		public String toString() {
			return "[id=" + id + ", name=" + name + ", age=" + age + "]\n";
		}

		@Override
		public int compareTo(User o) {
			return Integer.compare(this.age, o.age);
		}
	}
	
	static List<User> getUsers(){
		return Arrays.asList( 
				new User(10L, "ID", 36),
				new User(20L, "BJ", 32),
				new User(30L, "JM", 32),
				new User(40L, "PJ", 27),
				new User(50L, "HA", 25),
				new User(60L, "JE", 26),
				new User(70L, "JI", 31),
				new User(80L, "MP", 23)
				);
	}
	
	public static void main(String[] args) {
		List<User> users = getUsers();
		
		
		
		/*
		System.out.println("filter = "+
				filter(users,user->user.age>30)+"\n"
				+users);
		
		System.out.println("map = "+
				map(users, user->user.id));
		each(Arrays.asList(1,2,3,4,5),System.out::print);
		System.out.println();
		
		System.out.println("reduce = "+
				reduce(Arrays.asList(1,2,3,4,5), (a,b)->a+b, Integer.valueOf(0)));
		System.out.println("reduce = "+
				reduce(Arrays.asList(1,2,3,4,5), (a,b)->a+b));
		System.out.println("reduce = "+
				reduce(Arrays.asList(1), (a,b)->a+b, Integer.valueOf(0)));
		System.out.println("reduce = "+
				reduce(Arrays.asList(1), (a,b)->a+b));
		
		List<String> result = Stream.stream(users)
			.filter(user->user.age > 30)
			.map(user->user.name)
			.toList();
		System.out.println(result);
		//메서드 체이닝 방식과 차이 비교
		System.out.println("default = "+
					reduce(
							map(
								filter(users,user->user.age>30) 
								, user->user.age)
							, (a,b)->a+b));
		
		//기존은 안쪽에서부터 밖으로 나온다. 중첩구조가 심해질 수록 가독성이 안좋아진다.
		//스트림을 통한 메서드 체이닝 방식은 순차적으로 적용된다.
		
		System.out.println("stream = " +
				Stream.stream(users)
					.filter(user->user.age > 30)
					.map(user->user.age)
					.reduce(Integer::sum)
//					.reduce((a,b)->a+b)
				);
		*/
		/*
		//find, findIndex 실습
		System.out.println(find(users, user->user.age<32));
		System.out.println(find(users, user->user.age>55));
		System.out.println(findIndex(users, user->user.age<32));
		System.out.println(findIndex(users, user->user.age>55));
		System.out.println(Stream.stream(users).find(user->user.age<32));
		System.out.println(Stream.stream(users).find(user->user.age>55));
		System.out.println(Stream.stream(users).findIndex( user->user.age<32));
		System.out.println(Stream.stream(users).findIndex( user->user.age>55));
		
		System.out.println("every , find 테스트");
		System.out.println(some(users, u->u.age>30));
		System.out.println(some(users, u->u.age>50));
		System.out.println(every(users, u->u.age>30));
		System.out.println(every(users, u->u.age>10));
		System.out.println(Stream.stream(users).some(u->u.age>30));
		System.out.println(Stream.stream(users).some(u->u.age>50));
		System.out.println(Stream.stream(users).every(u->u.age>30));
		System.out.println(Stream.stream(users).every(u->u.age>10));
		System.out.println("pluck 테스트");
		System.out.println(pluck(users, "age",Integer.class));
		//타입불일치
		System.out.println(pluck(users, "age",String.class));
		System.out.println(Stream.stream(users).pluck("age",Integer.class));
		//타입불일치
		System.out.println(Stream.stream(users).pluck("age",String.class));
		
		System.out.println("reject 테스트");
		System.out.println(reject(users,u->u.age>30));
		System.out.println(Stream.stream(users).reject(u->u.age>30).toList());
		*/
		System.out.println("min, max, minBy, maxBy 테스트");
		System.out.println("나이 "+min(users, (u1,u2)->Integer.compare(u1.age,u2.age)));
		System.out.println("나이 "+max(users, (u1,u2)->Integer.compare(u1.age,u2.age)));
		System.out.println("이름 "+min(users, (u1,u2)-> u1.name.compareTo(u2.name) ));
		System.out.println("이름 "+max(users, (u1,u2)-> u1.name.compareTo(u2.name) ));
		System.out.println("나이 "+minBy(users, Integer::compare, u->u.age));
		System.out.println("나이 "+maxBy(users, Integer::compare, u->u.age));
		System.out.println(
				Stream.stream(users)
					.map(u->u.age)
					.max(Integer::compare));
		System.out.println(
				Stream.stream(users)
				.minBy(Long::compare,u->u.id));
	}
	
	/*
	 * filter는 처리 결과가 입력 결과와 타입은 같다.
	 * 길이는 같거나, 작을 수 밖에 없다.
	 */
	static <T> List<T> filter(List<T> list, Predicate<T> predi) {
		//함수형 프로그래밍은 원본 데이터를 수정하지 않는다. 새로운 데이터를 리턴하여
		//부수효과를 극단적으로 배제한다.
		ArrayList<T> newList = new ArrayList<>();
		each(list,data->{
			if(predi.test(data))
				newList.add(data);
		});
		
		return newList;
	}
	/*
	 * filter 정반대, 주어진 조건에 해당하지 않은 값만 걸러낸다.
	 */
	static <T> List<T> reject(List<T> list, Predicate<T> predi){
		return filter(list, predi.negate());
	}
	/*
	 * map은 처리 결과가 입력 결과와 타입은 같거나 다르다.
	 * 길이는 항상 같다.
	 */
	static <T,R> List<R> map(List<T> list, Function<T, R> mapper){
		ArrayList<R> newList = new ArrayList<>();
		each(list, data->newList.add(mapper.apply(data)));
		return newList;
	}
	/*
	 * 반복문을 중복을 대체할 함수
	 */
	static <T> List<T> each(List<T> list, Consumer<T> iter){
		for(T data : list) 
			iter.accept(data);
		return list;
	}
	
	/*
	 * 자바는 스트림 안에서 외부 변수를 참조하면 그 변수는 final 속성이 된다. 
	 * 따라서 갱신하면서 누산을 하지 못해, 간접적으로 값을 수정해야 한다.
	 */
	static <T> T reduce(List<T> list, BinaryOperator<T> reducer ,T memo) {
		//간소화된 유효성 검사, 본질을 흐리지 않는 선에서 간략화
		if(memo == null ) return reduce(list.subList(1, list.size()), reducer, list.get(0));
		if(list.size() < 2) return list.get(0);
		HashMap<Class<?>, T> map = new HashMap<>();
		map.put(memo.getClass(), memo);
		
		each(list, data->map.compute(memo.getClass(), (k,v)->reducer.apply(v, data)));
		return map.get(memo.getClass());
	}
	/*
	 * 자바는 람다에서 사용하는 변수는 final 속성을 띄기 때문에 memo 값 처리가 애매해진다.
	 * 삼항연산자 등 별도 처리하기보다 오버로딩으로 처리했다.
	 */
	static <T> T reduce(List<T> list, BinaryOperator<T> reducer) {
		if(list.size() < 2) return list.get(0);
		return reduce(list.subList(1, list.size()), reducer, list.get(0));
	}
	/*
	 * null 값을 간접적으로 다루기 위한 래퍼클래스 사용
	 */
	static <T> Optional<T> find(List<T> list, Predicate<T> predi) {
		for(int i=0;i<list.size();i++) {
			T value = list.get(i);
			if(predi.test(value)) return Optional.of(value);
		}
		return Optional.empty();//편의상 null리턴
	}
	static <T> Integer findIndex(List<T> list, Predicate<T> predi) {
		for(int i=0;i<list.size();i++) {
			if(predi.test(list.get(i))) return i;
		}
		return -1;
	}
	
	static <T> Boolean some(List<T> list, Predicate<T> predi) {
		return findIndex(list, predi) != -1;
	}
	static <T> Boolean every(List<T> list, Predicate<T> predi) {
		return findIndex(list, predi.negate()) == -1;
	}
	static <T> Boolean contains(List<T> list, T data){
		return findIndex(list, val-> Objects.equals(val, data) ) != -1 ;
	}
	static <T,R> List<R> pluck(List<T> list, String key,Class<R> typeToken){
		List<R> result = map(list, val-> pluckHelper(val, key, typeToken));
		return some(result, Objects::isNull).booleanValue() ? Collections.emptyList() :result;
	}
	//런타임 시 타입이 확정되는 경우 어쩔 수 없이 Object를 리턴해야 한다.
	static <T,R> R pluckHelper(T val,String key,Class<R> typeToken) {
		try {
			Field field = val.getClass().getDeclaredField(key);
			return typeToken.cast(field.get(val));
		} catch (Exception e) {
			return null;
		}
	}
	/*
	 * reduce 특화 함수
	 */
	static <T extends Comparable<T>> T min(List<T> list) {
		return reduce(list, (a,b)->a.compareTo(b)<=0 ? a : b );
	}
	static <T extends Comparable<T>> T max(List<T> list) {
		return reduce(list, (a,b)->a.compareTo(b)>0 ? a : b );
	}
	static <T> T min(List<T> list, Comparator<T> comparator) {
		return reduce(list, BinaryOperator.minBy(comparator));
	}
	
	static <T> T max(List<T> list, Comparator<T> comparator) {
		return reduce(list, BinaryOperator.maxBy(comparator));
	}
	//주어진 조건으로 검사한 결과로 최대 최소 값을 판단한다.
	static <T,R> T minBy(List<T> list,Comparator<R> comparator ,Function<T, R> mapper) {
		return reduce(list, (a,b)-> comparator.compare(mapper.apply(a), mapper.apply(b)) > 0 ? b:a);
	}
	static <T,R> T maxBy(List<T> list,Comparator<R> comparator ,Function<T, R> mapper) {
		return minBy(list,comparator.reversed(),mapper);
	}
	
	
	/*
	 * pipe, go 구현을 시도하려 했지만, 근본적으로 자바는 함수가 개념이 없어
	 * 호출부를 단일로 추상화할 수 없다. apply, test ... 
	 * 따라서 하나의 클래스로 묶었다.
	 */
	static class Stream<T> {
		List<T> list;
		
		static <T> Stream<T> stream(List<T> list){
			Stream<T> stream = new Stream<>();
			stream.list = list;
			return stream;
		}
		Stream<T> filter(Predicate<T> predi) {
			//함수형 프로그래밍은 원본 데이터를 수정하지 않는다. 새로운 데이터를 리턴하여
			//부수효과를 극단적으로 배제한다.
			ArrayList<T> newList = new ArrayList<>();
			forEach(data->{
				if(predi.test(data))
					newList.add(data);
			});
			return stream(newList);
		}
		Stream<T> reject(Predicate<T> predi){
			return filter(predi.negate());
		}
		<R> Stream<R> map(Function<T, R> mapper){
			ArrayList<R> newList = new ArrayList<>();
			forEach(data->newList.add(mapper.apply(data)));
			return stream(newList);
		}
		
		void forEach(Consumer<T> iter){
			for(T data : list) iter.accept(data);
		}
		
		Optional<T> reduce(BinaryOperator<T> reducer ,T memo) {
			//간소화된 유효성 검사, 본질을 흐리지 않는 선에서 간략화
			if(memo == null) return reduce(reducer);
			if(this.list.size() < 2) return Optional.of(this.list.get(0));
			HashMap<Class<?>, T> map = new HashMap<>();
			map.put(memo.getClass(), memo);
			each(this.list, data->map.compute(memo.getClass(), (k,v)-> reducer.apply(v, data)));
			return Optional.of(map.get(memo.getClass()));
		}
		Optional<T> reduce(BinaryOperator<T> reducer) {
			T tmpValue = this.list.get(0);
			if(this.list.size() < 2) return Optional.of(tmpValue);
			this.list = this.list.subList(1, list.size());
			return reduce(reducer, tmpValue);
		}
		
		Optional<T> find(Predicate<T> predi) {
			for(int i=0;i<this.list.size();i++) {
				T value = this.list.get(i);
				if(predi.test(value)) return Optional.of(value);
			}
			return Optional.empty();//편의상 null리턴
		}
		Integer findIndex(Predicate<T> predi) {
			for(int i=0;i<this.list.size();i++) {
				if(predi.test(this.list.get(i))) return i;
			}
			return -1;//편의상 null리턴
		}
		
		Boolean some(Predicate<T> predi) {
			return findIndex(predi) != -1;
		}
		Boolean every(Predicate<T> predi) {
			return findIndex(predi.negate()) == -1;
		}
		<R> List<R> pluck(String key,Class<R> typeToken){
			Stream<R> result = map(val-> pluckHelper(val, key, typeToken));
			return result.some(Objects::isNull).booleanValue() ? Collections.emptyList() :result.toList();
		}
		//런타임 시 타입이 확정되는 경우 어쩔 수 없이 Object를 리턴해야 한다.
		<R> R pluckHelper(T val,String key,Class<R> typeToken) {
			try {
				Field field = val.getClass().getDeclaredField(key);
				return typeToken.cast(field.get(val));
			} catch (Exception e) {
				return null;
			}
		}
		
		/*
		 * reduce 특화 함수
		 */
		Optional<T> min(Comparator<? super T> comparator) {
			return reduce(BinaryOperator.<T>minBy(comparator));
		}
		Optional<T> max(Comparator<? super T> comparator) {
			return reduce(BinaryOperator.<T>maxBy(comparator));
		}
		//주어진 조건으로 검사한 결과로 최대 최소 값을 판단한다.
		<R> Optional<T> minBy(Comparator<? super R> comparator ,Function<T, R> mapper) {
			return reduce( (a,b)-> comparator.compare(mapper.apply(a), mapper.apply(b)) > 0 ? b:a);
		}
		<R> Optional<T> maxBy(Comparator<? super R> comparator ,Function<T, R> mapper) {
			return minBy(comparator.reversed(),mapper);
		}
		
		List<T> toList(){
			return this.list;
		}
		
	}
	
	
}

 

 

헤드 퍼스트 디자인 패턴 | 에릭 프리먼 | 한빛미디어- 교보ebook

14가지 GoF 필살 패턴!, 경력과 세대를 넘어 오랫동안 객체지향 개발자의 성장을 도와준 디자인 패턴 교과서의 화려한 귀환! 》 2005년부터 디자인 패턴 도서 분야 부동의 1위 》 디자인 패턴의 고

ebook-product.kyobobook.co.kr

알고리즘을 캡슐화하는 패턴

 

public class TemplateMethodTest {
	static class Coffee{
		void prepareRecipe() {
			boilWater();
			brewCoffeeGrinds();
			purInCup();
			addSugarAndMilk();
		}
		
		public void boilWater() {
			System.out.println("물 끊이는 중");
		}
		public void brewCoffeeGrinds() {
			System.out.println("필터로 커피를 우려내는 중");
		}
		public void purInCup() {
			System.out.println("컵에 따르는 중");
		}
		public void addSugarAndMilk() {
			System.out.println("설탕과 우유를 추가하는 중");
		}
	}
	static class Tea {
		void prepareRecipe() {
			boilWater();
			steepTeaBag();
			purInCup();
			addLemon();
		}
		public void boilWater() {
			System.out.println("물 끊이는 중");
		}
		public void steepTeaBag() {
			System.out.println("찻잎을 우려내는 중");
		}
		public void addLemon() {
			System.out.println("레몬을 추가하는 중");
		}
		public void purInCup() {
			System.out.println("컵에 따르는 중");
		}
	}
	public static void main(String[] args) {
		Coffee coffee = new Coffee();
		Tea tea = new Tea();
		coffee.prepareRecipe();
		tea.prepareRecipe();
		
	}
}
물 끊이는 중
필터로 커피를 우려내는 중
컵에 따르는 중
설탕과 우유를 추가하는 중
물 끊이는 중
찻잎을 우려내는 중
컵에 따르는 중
레몬을 추가하는 중

알고리즘이 거의 같다. 두 클래스 공통된 부분을 추상화하는 것이 좋겠다.

public class TemplateMethodTest2 {
	
	//더 추상화 할 요소는?
	abstract static class CaffeineBeverage{
		abstract void prepareRecipe();
		public void boilWater() {
			System.out.println("물 끊이는 중");
		}
		public void purInCup() {
			System.out.println("컵에 따르는 중");
		}
	}
	
	static class Coffee extends CaffeineBeverage{
		void prepareRecipe() {
			boilWater();
			brewCoffeeGrinds();
			purInCup();
			addSugarAndMilk();
		}
		
		
		public void brewCoffeeGrinds() {
			System.out.println("필터로 커피를 우려내는 중");
		}
		
		public void addSugarAndMilk() {
			System.out.println("설탕과 우유를 추가하는 중");
		}
	}
	static class Tea extends CaffeineBeverage{
		void prepareRecipe() {
			boilWater();
			steepTeaBag();
			purInCup();
			addLemon();
		}
		public void steepTeaBag() {
			System.out.println("찻잎을 우려내는 중");
		}
		public void addLemon() {
			System.out.println("레몬을 추가하는 중");
		}
	}
}

더 추상화할 여지가 있다. 단순히 메서드 이름이 아니라 동작 자체를 보면 추상화할 것이 더 존재함을 알 수 있다.

무언가를 우려낸다는 행위와, 무언가를 첨가하는 것

public class TemplateMethodTest3 {
	//abstract 메서드가 없더라도 abstract를 붙여 인스턴스화를 방지할 수 있다.
	abstract static class CaffeineBeverage{
    	//핵심 템플릿 메서드 final
		final void prepareRecipe() {
			 boilWater();
			 brew();
			 purInCup();
			 addCondiments();
		}
		//약간의 동작 차이만을 보이므로 재정의를 강제하도록 abstract 키워드를 붙인다.
		protected abstract void addCondiments();
		protected abstract void brew();
		public void boilWater() {
			System.out.println("물 끊이는 중");
		}
		public void purInCup() {
			System.out.println("컵에 따르는 중");
		}
	}
	
	static class Coffee extends CaffeineBeverage{
		@Override
		public void addCondiments() {
			System.out.println("설탕과 우유를 추가하는 중");
		}
		@Override
		public void brew() {
			System.out.println("필터로 커피를 우려내는 중");
		}
	}
	static class Tea extends CaffeineBeverage{
		@Override
		protected void addCondiments() {
			System.out.println("레몬을 추가하는 중");
		}
		@Override
		protected void brew() {
			System.out.println("찻잎을 우려내는 중");
			
		}
	}
}

템플릿 메소드 패턴 알아보기

템플릿 메소드는 알고리즘의 각 단계를 정의하며, 서브 클래스에서 일부 단계를 구현할 수 있도록 유도한다.

핵심은 템플릿 메소드를 final로 선언해 각 알고리즘의 순서를 통제한다.

 

템플릿 메소드 패턴의 장점 

템플릿 메소드에서 알고리즘을 독점해 처리한다.

알고리즘이 한 군데 모여 있다. 

 

템플릿 메소드 패턴의 정의

알고리즘의 골격을 정의한다.

템플릿 메소드를 사용하면 알고리즘의 일부 단계를 서브클래스에서 구현할 수 있다.

알고리즘의 구조를 그대로 유지하면서 알고리즘의 특정 단계를 서브클래스에서 재정의할 수 있다.

abstract class AbstractClass{
    final void templateMethod() {
        primitiveOperation1();
        primitiveOperation2();
        concreteOperation();
        hook();
    }
    public void hook() {}
    public void concreteOperation() {}
    protected abstract void primitiveOperation2();
    protected abstract void primitiveOperation1();
}

템플릿 메소드 후크 알아보기

hook 메서드는 구상메서드지만, 구현한게 아무것도(혹은 거의없는) 없다.

hook 메서드는 알고리즘 사이사이에 마음것 위치할 있다.

서브클레스에서 필요 오버라이드해서 사용할 목적이다.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

import designpattern.headfirst.chapter8.TemplateMethodTest3.Coffee;
import designpattern.headfirst.chapter8.TemplateMethodTest3.Tea;

public class TemplateMethodTest4 {
	abstract static class CaffeineBeverageWithHook{
		final void prepareRecipe() {
			 boilWater();
			 brew();
			 purInCup();
			 if(customerWantsCondiments()) {
				 addCondiments();
			 }
		}
		boolean customerWantsCondiments() {
			return true;
		}
		protected abstract void addCondiments();
		protected abstract void brew();
		public void boilWater() {
			System.out.println("물 끊이는 중");
		}
		public void purInCup() {
			System.out.println("컵에 따르는 중");
		}
	}
	
	static class CoffeeWithHook extends CaffeineBeverageWithHook{
		@Override
		public void addCondiments() {
			System.out.println("설탕과 우유를 추가하는 중");
		}
		@Override
		public void brew() {
			System.out.println("필터로 커피를 우려내는 중");
		}
		
		@Override
		public boolean customerWantsCondiments() {
			String answer = getUserInput();
			
			if(answer.equalsIgnoreCase("y")) {
				return true;
			}else {
				return false;
			}
		}
		private String getUserInput() {
			String answer = null;
			System.out.print("커피에 우유와 설탕을 넣을까요?(y/n)");
			try{
				BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
				answer = br.readLine();
			}catch(IOException e) {
				System.out.println(e.getCause());
			}
			if(answer == null) {
				return "no";
			}
			return answer;
		}
	}
	static class TeaWithHook extends CaffeineBeverageWithHook{
		@Override
		protected void addCondiments() {
			System.out.println("레몬을 추가하는 중");
		}
		@Override
		protected void brew() {
			System.out.println("찻잎을 우려내는 중");
			
		}
		@Override
		public boolean customerWantsCondiments() {
			String answer = getUserInput();
			if(answer.equalsIgnoreCase("y")) {
				return true;
			}else {
				return false;
			}
		}
		private String getUserInput() {
			String answer = null;
			System.out.println("차에 레몬을 넣을까요? (y/n) ");
			try{
				BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
				answer = br.readLine();
			}catch(IOException e) {
				System.out.println(e.getCause());
			}
			if(answer == null) {
				return "no";
			}
			return answer;
		}
	}
	
	public static void main(String[] args) {
		Tea tea = new Tea();
		Coffee coffee = new Coffee();
		tea.prepareRecipe();
		coffee.prepareRecipe();
		
		TeaWithHook teaWithHook = new TeaWithHook();
		CoffeeWithHook coffeeWithHook = new CoffeeWithHook();
		
		System.out.println("\n홍차 준비중...");
		teaWithHook.prepareRecipe();
		System.out.println("\n커피 준비중...");
		coffeeWithHook.prepareRecipe();
	}
}
물 끊이는 중
찻잎을 우려내는 중
컵에 따르는 중
레몬을 추가하는 중
물 끊이는 중
필터로 커피를 우려내는 중
컵에 따르는 중
설탕과 우유를 추가하는 중

홍차 준비중...
물 끊이는 중
찻잎을 우려내는 중
컵에 따르는 중
차에 레몬을 넣을까요? (y/n) 
n

커피 준비중...
물 끊이는 중
필터로 커피를 우려내는 중
컵에 따르는 중
커피에 우유와 설탕을 넣을까요?(y/n)y
설탕과 우유를 추가하는 중

할리우드 원칙

먼저 연락하지 마세요. 저희가 연락 드리겠습니다.

 

의존성 부패를 방지하기 위함

의존성 부패는 고수준 구성요소가 저수준 구성 요소에 의존하고, 저수준 구성 요소는 다시 고수준 구성 요소에 의존하고를 반복하는 것 

순환 의존성

 

할리우드 원칙을 사용하면, 저수준 구성 요소가 시스템에 접속할 수는 있지만 언제, 어떻게 구성 요소를 사용할지는 고수준 구성 요소가 결정한다.

, 고수준 구성 요소가 저수준 구성 요소에게 "먼저 연락하지 마세요. 제가 먼저 연락 드리겠습니다." 라고 얘기하는 것과 같다.

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

할리우드 원칙과 템플릿 메소드 패턴의 관계는 쉡게 있다.

템플릿 메소드 패턴을 써서 디자인하면 자연스럽게 서브클래스에게 "우리가 연락할 테니까 먼저 연락하지 "라고 얘기하는 셈이다.

 

자바 API 템플릿 메소드 패턴 알아보기

    public static void sort(Object[] a) {
        if (LegacyMergeSort.userRequested)
            legacyMergeSort(a); //일부를 위임
        else
            ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
    }

    /** To be removed in a future release. */
    private static void legacyMergeSort(Object[] a) {
        Object[] aux = a.clone();
        mergeSort(aux, a, 0, a.length, 0);
    }

Arrays.sort 는 알고리즘을 구현하고, 일부 단계를 서브클래스에서 구현하라는 템플릿 메서드 정의와 완전히 같지는 않다.

자바에서 모든 배열이 정렬 기능을 사용할 수 있도록 만들기 위해 정적 메서드 sort를 만들고 대소를 비교하는 부분은 배열 타입 객체에서 구현하도록 만들었다.

이런 점에서 sort()메소드 구현 자체는 템플릿 메소드 패턴의 기본 정신에 충실하다.

Arrays의 서브클래스를 만들어야 한다는 제약조건이 없어 더 유연하기도 하다.

단점

	public class ArraysSortTest {
	    static class Dummy {}
	    
	    public static void main(String[] args) {
	        Dummy[] dumArr = {new Dummy(), new Dummy()};
	        Arrays.sort(dumArr);
	    }
	}
	
	Exception in thread "main" java.lang.ClassCastException: class designpattern.headfirst.chapter8.ArraysSortTest$Dummy cannot be cast to class java.lang.Comparable (designpattern.headfirst.chapter8.ArraysSortTest$Dummy is in unnamed module of loader 'app'; java.lang.Comparable is in module java.base of loader 'bootstrap')
	 at java.base/java.util.ComparableTimSort.countRunAndMakeAscending(ComparableTimSort.java:320)
	 at java.base/java.util.ComparableTimSort.sort(ComparableTimSort.java:188)
	 at java.base/java.util.Arrays.sort(Arrays.java:1041)
	 at designpattern.headfirst.chapter8.ArraysSortTest.main(ArraysSortTest.java:10)
	
	sort 메서드에서 해당 객체 배열이 Comparable 을 구현했는지 컴파일러에서 체크를 할 수 없어 런타임 에러를 유발한다…

핵심 정리

템플릿 메소드는 알고리즘의 단계를 정의하며 일부 단계를 서브 클래스에서 구현하도록 할 수 있다.

템플릿 메소드 패턴은 코드 재사용에 도움된다.

후크 메서드는 서브 클래스에서 선택적으로 재정의한다.

할리우드 원칙에 의하면, 저수준 모듈은 언제 어떻게 호출할지는 고수준 모듈에서 결정해야 한다.

실전에서 교과서적으로 패턴과 반드시 일치하지 않을 수 있다. 

팩토리 메소드 패턴은 템플릿 메소드 패턴의 특화 버전이다.

+ Recent posts