동작 파라미터화를 이용하면 자주 바뀌는 요구사항에 효과적으로 대응 할 수 있다.
동작 파라미터화란 아직은 어떻게 실행 할 것인지 결정하지 않은 코드 블록을 의미한다. 즉, 코드블록에 따라 메서드의 동작이 파라미터화된다.
2.1 변화하는 요구사항에 대응하기
2.1.1 첫번째 시도 : 녹색 사과를 필터링
public enum Color {
RED, GREEN
}
public static List<Apple> filterGreenApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<>(); // 사과 누적 리스트
for (Apple apple : inventory) {
if (Color.GREEN.equals(apple.getColor())) { // 녹색사과 만 선택
result.add(apple);
}
}
return result;
}
녹색사과를 선택하는데 필요한 조건에서 녹색사과 말고 빨간 사과도 필터링을 하게 변화한다. 크게 고민하지않는다면 , 해당 메서드를 복사하여 빨간사과를 필터링 할 수 있지만, 나중에 다양한 색을 필터링 시에는 적절히 대응 할 수 없다.
2.1.2 두번째 시도 : 색을 파라미터화
public static List<Apple> filterGreenApples(List<Apple> inventory, Color color) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (apple.getColor().equals(color)) {
result.add(apple);
}
}
return result;
}
List<Apple> greenApples = filterApplesByColor(inventory, Color.GREEN);
List<Apple> redApples = filterApplesByColor(inventory, Color.RED);
해당 형식으로 색을 파라미터화 하였다. 하지만 색 이외에도 가벼운 사과와 무거운 사과로 구분하는 조건이 추가되었다.
보통 무게가 150g 이상인 사과가 무거운 사과이다.
앞선 요구사항을 듣다보면 색과 마찬가지로 앞으로 무게의 기준도 얼마든지 바뀔 수 있다.
그러면 어떠한 기준으로 사과를 필터링 할 것인가? 색이나 무게중 어떤것을 기준으로 필터링할지 가리키는 플래그를 추가 할 수 있다.
2.1.3 세번째 시도 : 가능한 모든 속성으로 필터링
public static void main(String[] args) {
List<Apple> inventory = null;
List<Apple> greenApples = filterApple(inventory, Color.GREEN, 0, true);
List<Apple> redApples = filterApple(inventory, null, 150, false);
}
public static List<Apple> filterApple(List<Apple> inventory, Color color, int weight, boolean 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;
}
true, false는 무엇을 의미하는걸까,,
가독성이 좋지않다. 이해하기 어렵다. 요구사항이 변경시 어떻게 바꿔야할까? 라는 생각이 든다.
2.2 동작 파라미터화
앞선 예제를 좀더 유연하게 대응 하는 방법으로, 참 또는 거짓을 반환하는 함수 프레디케이트 인터페이스를 정의하자.
※ 프레디케이트 : 논리학에서 프레디케이트는 일반적으로 특정 주어에 대해 참(true)이나 거짓(false)인지를 나타내는 함수이다.
public interface ApplePredicate {
boolean test(Apple apple);
}
public class AppleHeavyWeightPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return apple.getWeight() > 150; // 무거운 사과인지 판별
}
}
public class AppleGreenColorPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return Color.GREEN.equals(apple.getColor()); // 녹색 사과인지 판별
}
}
사과를 선택하는 다양한 전략 이를, 전략 디자인 패턴 이라고한다.
전략 디자인 패턴은 각 알고리즘을 캡슐화하는 알고리즘 패밀리를 정의해둔 다음에 런타임에 알고리즘을 선택하는 기법이다.
알고리즘 패밀리 : 인터페이스 ApplePredicate / 구현체는 전략
2.2.1 네번째 시도 : 추상적 조건으로 필터링
public static List<Apple> filterApple(List<Apple> inventory, ApplePredicate applePredicate) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (applePredicate.test(apple)) { // ApplePredicate 객체를 이용한 사과 검사 조건
result.add(apple);
}
}
return result;
앞전의 코드에 비해 유연한 코드와 가독성이 좋아졌다.
Ex) 150g이 넘는 빨간사과를 검색하는 요구사항시,
public class AppleRedAndHeavyPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return Color.RED.equals(apple.getColor()) && apple.getWeight() > 150;
}
}
구현이 가능하다.
해당 메서드는 test메서드이다. 메서드는 객체만 인수로 받으므로 test 메서드를 ApplePredicate 객체로 감싸서 전달해야한다.
2.3 복잡한 과정 간소화
public class FilteringApples {
public static void main(String[] args) {
List<Apple> inventory = Arrays.asList(new Apple(80, Color.GREEN),
new Apple(155, Color.GREEN),
new Apple(120, Color.RED));
List<Apple> heavyApples = filterApples(inventory, new AppleHeavyWeightPredicate());
List<Apple> greenApples = filterApples(inventory, new AppleGreenColorPredicate());
System.out.println("heavyApples value = " + heavyApples);
System.out.println("greenApples value = " + greenApples);
}
public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate applePredicate) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (applePredicate.test(apple)) {
result.add(apple);
}
}
return result;
}
}
새로운 동작을 전달하려면 ApplePredicate 인터페이스를 구현하는 여러 클래스를 정의해야한다. 상당히 번거로우며, 로직과 관련 없는 코드가 많이 추가된다. 자바는 클래스 선언과 인스턴스화를 동시에 수행 할 수 있는 익명 클래스가 있다.
2.3.1 익명 클래스
익명클래스는 말 그대로 이름이 없는 클래스이다. 익명클래스를 이용시 필요한 구현을 만들어 사용이 가능하다.
2.3.2 다섯번째 시도 : 익명 클래스 사용
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
@Override
public boolean test(Apple apple) {
return Color.RED.equals(apple.getColor());
}
});
익명클래스를 사용하여도 첫째, 엄청난 변화가 있지는 않다, 여전히 많은 공간을 차지한다.
둘째, 많은 사람들이 익명 클래스의 사용에 익숙하지 않다.
장황한 코드는 구현하고, 유지보수하는데 시간이 오래 걸리며, 가독성이 좋지않다.
2.3.3 여섯번째 시도 : 람다 표현식 사용
람다 표현식을 사용한 재구현이다.
List<Apple> result =
filterApples(inventory, (Apple apple) -> Color.RED.equals(apple.getColor()));
이전코드보다 훨씬 간단하고, 가독성이 좋다👍🏻
2.3.4 일곱번째 시도 : 리스트 형식으로 추상화
public class TestClass {
/**
* 제네릭 사용
*
* 데이터 타입에 의존적이지 않고,
* 외부에 의해 타입을 지정
*/
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;
}
}
이제 다양한 타입을 필터 메서드로 사용 할 수 있다.
Ex) 람다표현식
List<Apple> redApples =
filter(inventory, (Apple apple) -> Color.RED.equals(apple.getColor()));