한빛 미디어의 [모던 자바 인 액션]의 chapter2를 읽고 정리한 글입니다.
우리가 어떤 환경에서 일을 하든 요구사항은 항상 바뀐다.
변화하는 요구사항에 빠르게 대응하기 위해서는, 새로 추가한 기능은 쉽게 구현할 수 있어야 하며 장기적인 관점에서 유지보수가 쉬워야 한다.
동작 파라미터화(behavior parameterization)
을 이용하면 자주 바뀌는 요구사항에 효과적으로 대응할 수 있다.
예제 : 사과를 필터링해보자
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;
}
그런데 갑자기 녹색 사과 말고 빨간 사과로 필터링 하고 싶다면?
-> 메서드를 통으로 복사해서 filterRedApples 메소드 만들고, if문 조건을 빨간 사과로 바꾸기?
-> 그런데 이번엔 노란 사과로 필터링 하고 싶다면?
-> ... 무한 복사?
=> 말도 안된다 🤯
이럴때 사용하기 좋은 규칙이 있다
거의 비슷한 코드가 반복 존재한다면 그 코드를 추상화한다
어떻게 해야 filterGreenApples의 코드를 반복 사용하지 않고 filterRedApples를 구현할 수 있을까?
색을 파라미터화할 수 있도록 메서드에 파라미터를 추가
-> 변화하는 요구사항에 좀 더 유연하게 대응할 수 있다
public static List<Apple> filterApplesByColor (List<Apple> inventory, Color color) {
List<Apple> result = new ArrayList<>();
for (Apple apple: inventory) {
if (apple.getColor().equals(color)) {
result.add(apple);
}
}
return result;
}
filterApplesByColor(inventory, GREEN);
filterApplesByColor(inventory, RED);
그런데 갑자기 무게가 150그램 이상인 사과를 필터링하고 싶다면?
-> filterApplesByColor를 복사한 filterApplesByWeight 만들기?
-> 근데 다른 조건 또 생기면?
-> ... 무한복사? (+수없는 중복)
=> 이 역시 말도 안된다 🤯
더 좋은 방법이 없을까❓❓❓
파라미터를 추가하는 방법말고, 변화하는 요구사항에 좀 더 유연하게 대응할 수 있는 방법이 없을까?
선택 조건을 결정하는 인터페이스
를 정의하자!
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());
}
}
ApplePredicate
AppleHeavyWeightPredicate
, AppleGreenColorPredicate
이렇게 동작 파라미터화
(메서드가 다양한 동작을 받아서 내부적으로 다양한 동작을 수행) 할 수 있다.
public static List<Apple> filterApples (List<Apple> inventory, ApplePredicate p) {
List<Apple> result = new ArrayList<>();
for (Apple apple: inventory) {
if (p.test(apple)) { // Predicate 객체로 사과 검사 조건 캡슐화
result.add(apple);
}
}
return result;
}
List<Apple> HeavyApples = filterApples(inventory, new AppleHeavyWeightPredicate());
이렇게 하면 filterApples 내부에서 컬렉션을 반복하는 로직
과 컬렉션의 각 요소에 적용할 동작
(Predicate)을 분리할 수 있다.
우리가 전달한 ApplePredicate
객체에 의해 filterApples
메서드의 동작이 결정된다.
= filterApples
메서드의 동작을 파라미터화 했다.
=> 한 메서드가 다른 동작을 수행하도록 재활용할 수 있다.
훨씬 유연하고!!
훨씬 가독성 좋고!!
훨씬 사용하기 편한!!
멋쟁이 코드가 만들어졌다😎
이제 새로운 필터링 조건이 생기더라도~
우리는 ApplePredicate를 적절하게 구현하는 클래스
만 만들면 끝!
.
.
.
그런데.. test메서드 구현을 위해 ApplePredicate 구현 클래스를 계속 만드는 거.. 귀찮지 않나?
현재 filterApples 메서드로 새로운 동작을 전달하려면 ApplePredicate 인터페이스를 구현하는 여러 클래스를 정의한 다음에 인스턴스화해야한다.
-> 상당히 귀찮다 😩
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
public boolean test(Apple apple) {
return RED.equals(apple.getColor());
}
});
근데 익명 클래스에도 아직 단점이 있다.
1. 여전히 많은 공간을 차지한다.
2. 많은 프로그래머가 익명 클래스의 사용에 익숙하지 않다. (장황한 코드)
.
.
.
더 간단한거 없을까? 🥺
자바8의 람다 표현식을 이용해서 위 예제 코드를 다음처럼 재구성할 수 있다.
List<Apple> redApples = filterApples(inventory,
(Apple apple) -> return RED.equals(apple.getColor());
초간단해졌다!!!
아주 멋지다 우하하 😎
동작 파라미터화는 값 파라미터화에 비해 훨씬 유연하다
public class AppleGreenColorPredicate implements ApplePredicate {
public boolean test(Apple apple) {
return GREEN.equals(apple.getColor());
}
List<Apple> redApples = filterApples(inventory, new AppleGreenColorPredicate());
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
public boolean test(Apple apple) {
return GREEN.equals(apple.getColor());
}
});
List<Apple> redApples = filterApples(inventory,
(Apple apple) -> return GREEN.equals(apple.getColor());
ㅋㅋㅋㅋ공통적인 부분을 묶어서 빼내서,
코드 재사용성을 높이는 방법이구나