동작 파라미터화 코드 전달하기 #
쉽게 말하면 메서드를 파라미터로 전달하는 것이다.
동작 파라미터화란? 아직은 어떻게 실행할 것인지 결정하지 않은 코드블록이다. 코드블록은 나중에 프로그램에서 호출한다. 즉, 코드블록의 실행은 나중이다.
예시코드 #
첫 번째 시도 #
public static List<Apple> filterApple(List<Apple> apples) {
List<Apple> result = new ArrayList<>();
for(Apple apple : apples) {
if(GREEN.equals(apple.getColor())) {
result.add(apple);
}
}
return result;
}
여기서 조건(GREEN)이 변경(RED)되거나 추가(Weight)된다면 해당 메서드를 사용할 수 없다. (새로운 메서드를 생성할 것이다.) 새로운 메서드를 생성하면 조건 검사 로직 외에는 모두 중복코드이다. 따라서 해당 부분을 파라미터화 하여 개선해볼 수 있다.
두 번째 시도 #
public static List<Apple> filterGreenApple(List<Apple> apples, String color) {
List<Apple> result = new ArrayList<>();
for(Apple apple : apples) {
if(color.equals(apple.getColor())) {
result.add(apple);
}
}
return result;
}
public static List<Apple> filterApple(List<Apple> apples, int weight) {
List<Apple> result = new ArrayList<>();
for(Apple apple : apples) {
if(weight <= apple.getWeight()) {
result.add(apple);
}
}
return result;
}
위의 코드로 ‘첫 번째 시도’ 를 개선해보았다. 위의 코드는 다시 아래와 같이 수정해볼 수 있다.
이는 소프트웨어 공학의 DRY(Don’t Repeat yourself) 같은 것을 반복하지말 것 원칙을 어기는 것이다. 이를 해결하기 위해서 색과 무게를 filter라는 메서드로 합치는 방법도 있다. 따라서 색이나 무게 중 어던 것을 기준으로 필터링할 지 가리키는 플래그를 추가할 수 있다. (하지만 실전에서는 절대 아래의 방법을 사용하지 말아야 한다.)
세 번째 시도 #
public static List<Apple> filterApple(List<Apple> apples, String color, int weight, boolean flag) {
List<Apple> result = new ArrayList<>();
for(Apple apple : apples) {
if((flag && color.equals(apple.getColor())) || (!flag && weight <= apple.getWeight()))
result.add(apple);
}
return result;
}
List<Apple> greenApples = filterApple(apples, "GREEN", 100, true);
List<Apple> heavyApples = filterApple(apples, "GREEN", 100, false);
위와 같은 코드 방식은 절대 사용하지 말라고 권장된다. 과연 true/false
가 의미하는 것이 명확하다고 할 수 있을까라는 것이다. 또, 요구사항이 다시 변경된다면 의미가 없어진다. (예를 들어, 사과의 모양, 원산지 등으로 필터링 조건이 추가된다면..?)
네 번째 시도 (동작 파라미터화) #
public interface ApplePredicate {
boolean test(Apple apple);
}
public class AppleHeavyWeightPredicate implements ApplePredicate { // 전략
public boolean test(Apple appe) {
return apple.getWeight() > 150;
}
}
public class AppleGreenColorPredicate implements ApplePredicate { // 전략
public boolean test(Apple apple) {
return "GREEN".equals(apple.getColor());
}
}
// 조건이 추가/변경될 때, 위와 같은 Predicate 만 생성해주고 적용하면 된다.
public static List<Apple> filterApple(List<Apple> apples, ApplePredicate p) {
List<Apple> result = new ArrayList<>();
for(Apple apple : apples) {
if(p.test(apple)) {
result.add(apple);
}
}
return result;
}
위와 같은 방식을 전략 패턴이라고 한다. 전략(알고리즘)을 동적으로 선택한다.
전략 디자인 패턴은 각 알고리즘(전략이라 부르는)을 캡슐화 하는 알고리즘 패밀리를 정의해둔 다음에 런타임에 알고리즘을 선택하는 기법이다.
클라이언트는 다양한 전략들 중 한 개를 선택해서 생성하고, 컨텍스트에 주입한다. (사용한다.)
다섯 번째 시도 (동적 파라미터 -> 익명 클래스 적용) #
List<Apple> filteredApples = filterApple(apples, new ApplePredicate() {
public boolean test(Apple apple) {
return "GREEN".eqauls(apple.getColor());
}
});
Predicate 를 생성하기에는 애매할 때, 위와 같이 익명 클래스로 사용할 수 있다.
다만 익명 클래스의 단점은 아직도 코드의 길이가 길다는 것이다. (또, 익숙하지 않다는 것도 있다.)
여섯 번째 시도 (동적 파라미터 -> 익명 클래스 적용) #
List<Apple> filteredApples = filterApple(apples, apple -> "GREEN".eqauls(apple.getColor()));
Predicate 를 생성하기에는 애매할 때, 위와 같이 람다표현식으로 사용할 수 있다. (익명 클래스보다 더 가독성이 뛰어나고 사용하기 쉽다.)
위와 같이 자주 사용되는 Predicate, Function 등을 이미 자바에서 함수형 인터페이스로써 만들어두었다.
List<Apple> filteredApples = apples.filter(apple -> "GREEN".eqauls(apple.getColor()));
사실 위에서 만든 filterApple()
함수를 다른 List, Predicate 형식으로 추상화하면 자바에서 제공하는 filter()
가 된다.
public static List<T> filter(List<T> list, Predicate<T> p) {
List<T> result = new ArrayList<>();
for(T el : list) {
if(p.test(el)) {
result.add(el);
}
}
return result;
}
Comparator 로 정렬하기 #
java.util.Comparator
객체를 이용하여 sort 에서 동작 파라미터를 사용할 수 있다.
public interface Comparator<T> {
int compare(T o1, T o2);
}
apples.sort(new Comparator<Apple> {
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(a2.getWeight());
}
})
이것은 최종적으로 아래와 같이 수정할 수 있다.
apples.sort((Apple o1, Apple o2) -> a1.getWeight().compareTo(a2.getWeight()));