챕터 2는 앞서 챕터 1에서 간단히 소개 되었던 동작 파라미터화에 관한 이야기이다.
예를 들어 나중에 실행될 메서드의 인수로 코드 블록을 전달 할 수 있다. 챕터 2에서는 이전까지 동작 파라미터화의 문제점을 자바 8에서는 어떻게 해결하였는지 보여준다. 아래 예제들을 보며 이해해보자.
만약 사과를 재배하는 농부가 있고, 초록 사과를 필터링 하는 코드를 작성해야 한다고 하자. 그러면 다음과 같이 코드를 작성할 수 있다.
enum Color { RED, GREED }
public static List<Apple> filterGreenApples(List<Apple> inventory){
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory){
if (GREEN.equals(apple.getColor()){
result.add(apple);
}
}
return result;
}
이렇게 초록 사과를 필터링 하는 코드를 작성하였다. 하지만 만약 빨간 사과를 필터링하고 싶으면 어떻게 해야 될까? 또는 더 나아가 좀 더 다양한 색(초록색, 빨간색, 노란색 등)을 필터링 하고 싶다면?
하지만 이는 코드의 가독성을 낮추고 반복적인 코드가 늘어난다.
거의 비슷한 코드가 반복 존재한다면 그 코드를 추상화해보자.
public static List<Apple> filterGreenApples(List<Apple> inventory, Color color){
//Color를 파라미터화 하였다.
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory){
if (apple.getColor().equals(color){
//색을 파라미터화 하여 인수로 넘겨 받는다.
result.add(apple);
}
}
return result;
}
하지만 갑자기 농부가 색 이외에도 사과의 무게도 필터링 하고 싶다면?
public static List<Apple> filterGreenApples(List<Apple> inventory, Color color,
int weight, boolean flag){
//Color, Weight 파라미터화, flag로 색깔, 무게 중 하나 필터링
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory){
if((flag && apple.getColor().equals(color)) ||
(!flag && apple.getWeight() > weight){
result.add(apple);
}
}
return result;
}
List<Apple> greenApples = filterApples(inventory, GREEN, 0, true);
List<Apple> heavyApples = filterApples(inventory, null, 150, false);
여기서 문제점이 무엇일까?
결국 중복된 필터 메서드를 만들거나 모든 것을 처리하는 거대한 필터 메서드를 구현해야 한다.
아래와 같이 선택 조건을 결정하는 인터페이스를 정의해보자
public interface ApplePredicate {
boolean test (Apple apple);
}
위 인터페이스를 통해 다양한 선택 조건을 구현할 수 있다.
//무거운 사과를 필터링 하는 메서드
public class AppleHeavyWeightPredicate implements ApplePredicate {
public boolean test(Apple apple) {
return apple.getWeight() > 150;
}
}
//녹색 사과를 필터링 하는 메서드
public class AppleGreenColorPredicate implements ApplePredicate {
public boolean test(Apple apple) {
return GREEN.equals(apple.getColor());
}
}
위와 같이 참 또는 거짓을 반환하는 함수를 프리디케이트(Predicate)라고 한다.
위와 같이 코드를 작성하였을때 어떻게 다양한 동작을 수행 할 수 있을까?
아래는 수정한 filterApples이다.
public static List<Apple> filterApples(List<Apple> inventory,
ApplePredicate p) {
//ApplePredicate 객체를 통해 동작을 전달 받는다.
List<Apple> result = new ArrayList<>();
for(Apple apple : inventory){
if(p.test(apple)) {
result.add(apple);
}
}
return result;
}
하지만 여기에서도 문제점은 있다.
이는 상당히 번거로운 작업이다. 한번 익명 클래스를 사용하여 다시 수정해보자.
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
public boolean test(Apple apple) {
return RED.equals(apple.getColor());
}
});
익명 클래스를 활용하여 클래스 선언과 인스턴스화를 동시에 하였다.
하지만 이도 단점은 있다.
따라서 Java 8에서 추가된 람다 표현식을 사용하면 이를 해결 할 수 있다.
List<Apple> result =
filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));
처음과 비교하였을 떄 훨씬 가독성이 높아지고 코드도 간결해졌다.
또한 리스트 형식으로 추상화하여 다른 형식의 리스트에 필터 메서드를 사용할 수도 있다.
public interface Predicate<T> {
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> result = new ArrayList<>();
for(T e: list) {
if(p.test(e)) {
result.add(e);
}
}
return result;
}
이제 바나나, 오렌지, 정수, 문자열 등의 리스트에 필터 메서드를 사용할 수 있다.
//사과 색깔 필터링
List<Apple> redApples =
filter(inventory, (Apple apple) -> RED.equals(apple.getColor()));
//정수 필터링
List<Integer> evenNumbers =
filter(numbers, (Integer i) -> i % 2 == 0);
동작 파라미터화 패턴은 동작을 (한 조각의 코드로) 캡슐화한 다음에 메서드로 전달하여 메서드의 동작을 파라미터화 한다.