람다 표현식으로 전략, 템플릿 메서드, 옵저버, 팩토리 등의 객체지향 디자인 패턴을 어떻게 간소화 시킬지
람다 표현식과 스트림 API를 사용하는 코드를 테스트하고 디버깅하는 방법
9.1 가독성과 유연성을 개선하는 리팩터링
9.1.1 코드 가독성 개선
람다, 메서드 참조, 스트림을 황용해서 코드 가동성 개선 예제 3가지
익명 클래스를 람다 표현식으로 리팩터링
람다 표현식을 메서드 참조로 리팩터링
명령형 데이터 처리를 스트림으로 리팩터링
9.1.2 익명 클래스를 람다 표현식으로 리팩터링
익명 클래스는 코드를 장황하게 만들고 쉽게 에러를 일으킨다.
람다 표현식으로 간결하고 가독성이 좋은 코드를 만들 수 있다.
Runnable r1 = new Runnable() {
public void run() {
System.out.println("Hello");
}
}
Runnable r2 = () -> System.out.println("Hello");
하지만 모든 익명 클래스를 람다 표현식으로 변환할 수 있는 것은 아니다.
익명 클래스에서 사용한 this와 super는 람다 표현식에서 다른 의미: 익명 클래스에서 this는 익명 클래스 자신을 가리키지만, 람다에서 this는 람다를 감싸는 클래스 가리킴
익명 클래스는 감싸고 있는 클래스의 변수를 가릴 수 있지만(shadowing 가능), 람다 표현식으로는 람다를 감싸고 있는 클래스의 변수를 가릴 수 없기 때문에 컴파일 에러
int a = 10;
Runnable r1 = () -> {
int a = 10; // 컴파일 에러
System.out.println(a);
}
Runnable r2 = new Runnable(){
public void run(){
int a = 2;
System.out.println(a);
}
}
익명 클래스를 람다 표현식으로 바꾸면 콘텍스트 오버로딩에 따른 모호함이 초래될 가능성
interface Task{
public void execute();
}
public static void doSomething(Runnable r){
r.run();
}
public static void doSomething(Task t){
t.execute();
}
doSomething(new Task(){
public void execute(){
System.out.println("Danger");
}
})
익명 클래스를 람다 표현식으로 바꾸면 Runnable과 Task 모두 대상 형식이 될 수 있으므로 문제가 생긴다.
class Feed implements Subject {
private final List<Observer> observers = new ArrayList<>();
public void registerObserver(Observer o) {
this.observers.add(o);
}
public void notifyObservers(String tweet) {
observers.forEach(
o -> o.notify(tweet);
);
}
}
interface Observer {
void notify(String tweet);
}
class NYTimes implements Observer {
public void notify(String tweet) {
if(tweet != null && tweet.contains("money")){
System.out.println("Breaking news in NY! " + tweet);
}
}
}
class Guardian implements Observer {
public void notify(String tweet) {
if(tweet != null && tweet.contains("queen")){
System.out.println("Yet more news from London... " + tweet);
}
}
}
class NYTimes implements Observer {
public void notify(String tweet) {
if(tweet != null && tweet.contains("wine")){
System.out.println("Today cheese, wine and news! " + tweet);
}
}
}
Feed f = new Fee();
f.registerObserver(new NYTimes());
f.registerObserver(new Guardian());
f.registerObserver(new LeMonde());
f.notifyObservers("The queen said her favorite book is Modern Java in Action");
한 객체가 어떤 작업을 처리한 다음에 다른 객체로 결과를 전달하고, 다른 객체도 해야할 작업을 처리한 다음에 또 다른 객체로 전달하는 식
일반적으로 다음으로 처리할 객체 정보를 유지하는 필드를 포함하는 작업 처리 추상 클래스로 의무 체인 패턴을 구성한다.
작업 처리 객체가 자신의 작업을 끝냈으면 다음 작업 처리 객체로 결과를 전달
public abstract class ProcessingObject<T> {
protected ProcessingObject<T> successor;
public void setSuccessor(ProcessingObject<T> successor) {
this.successor = successor;
}
public T handle(T input) {
T r = handleWork(input);
if(successor != null) {
return successor.handle(r);
}
}
abstract protected T handleWork(T input);
}
ProcessingObject 클래스를 상속받아 handleWork 메서드를 구현하여 다양한 종류의 작업 처리 객체를 만들 수 있다.
public class HeaderTextProcessing extends ProcessingObject<String> {
protected String handleWork(String text) {
return "From Raoul, Mario and Alan: " + text;
}
}
public class SpellCheckerProcessing extends ProcessingObject<String> {
protected String handleWork(String text) {
return text.replaceAll("labda", "lambda");
}
}
ProcessingObject<String> p1 = new HeaderTextProcessing();
ProcessingObject<String> p2 = new SpellCheckerProcessing();
p1.setSuccessor(p2);
String result = p1.handle("Aren't labdas really sexy?");
System.out.println(result);
// From Raoul, Mario and Alan: Aren't lambdas really sexy?
람다 표현식 사용
의무 체인 패턴은 함수 체인, 함수 조합과 비슷하다.
UnaryOperator< T > 타입의 작업 처리 객체들을 andThen 메서드로 연결, 조합하여 Function< T, R> 타입의 체인 객체를 만들 수 있다.
인스턴스화 로직을 클라이언트에 노출하지 않고 객체를 만들 때 팩토리 디자인 패턴을 사용한다.
public class ProductFactory {
public static Product createProduct(String name) {
switch(name) {
case "loan" : return new loan();
case "stock" : return new Stock();
case "bond" : return new Bond();
default: throw RuntimeException("No such product" + name);
}
}
}
팩토리 디자인 패턴 코드의 장점은 생성자와 설정을 외부로 노출하지 않음으로써 클라이언트가 단순하게 상품을 클라이언트가 단순하게 상품을 생산할 수 있다는 것이다.
Product p = Product.createProduct("loan");
람다 표현식 사용
생성자도 메서드 참조처럼 접근 가능
public class ProductFactory {
final static Map<String, Supplier<Product>> map = new HashMap<>();
static {
map.put("loan", Loan::new);
map.put("stock", Stock::new)
map.put("bond", Bond::new)
}
public static Product createProduct(String name) {
Suppllier<Product> p = map.get(name);
if(p != null) return p.get();
throw new IllegalArgumentException("No such product " + name);
}
}
팩토리 메서드 createProduct가 상품 생성자로 여러 인수를 전달하는 상황에서는 Supplier 함수형 인터페이스가 아닌 여러 인수를 전달받는 추상 메서드를 갖는 함수형 인터페이스를 선언해 사용해야 한다.
@FunctionalInterface
public interface TriFunctino<T, U, V, R> {
R apply(T t, U u, V v);
}
Map<String, TriFunction<Integer, Integer, String, Product>> map
= new HashMap<>();
9.3 람다 테스팅
public class Point {
private final int x;
private final int y;
private Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
public Point moveRightBy(int x) {
return new Point(this.x + x, this.y)
}
}
moveRightBy 메서드가 의도한 대로 동작하는 지 확인하는 단위 테스트
@Test
public void testMoveRightBy() throws Exception {
Point p1 = new Point(5, 5);
Point p2 = p1.moveRightBy(10);
assertEquals(15, p2.getX());
assertEquals(5, p2.getY());
}
9.3.1 보이는 람다 표현식의 동작 테스팅
람다는 익명이므로 람다 자체를 테스트 코드 이름으로 호출할 수 없다.
필요하다면 람다를 필드에 저장해서 재사용할 수 있으며 람다의 로직을 테스트할 수 있다.
이렇게 하면 메서드를 호출하는 것처럼 람다를 사용할 수 있다.
Point 클래스에 Compartor< Point > 타입의 정적 필드 compareByXAndThenY 선언
public class Point {
public final static Comparator<Point> compareByXAndThenY =
comparing(Point::getX).thenComparing(Point::getY);
}
람다 표현식은 함수형 인터페이스의 인스턴스를 생성한다.
생성된 인스턴스의 동작으로 람다 표현식을 테스트할 수 있다.
@Test
public void testComparingTwoPoints() throws Exception {
Point p1 = new Point(10, 15);
Point p2 = new Point(10, 20);
int result = Point.compareByXAndThenY.compare(p1, p2);
assertTrue(result < 0);
}
9.3.2 람다를 사용하는 메서드의 동작에 집중하라
람다 표현식을 사용하는 메서드의 동작을 테스트함으로써 람다를 공개하지 않으면서도 람다 표현식을 검증할 수 있다.
public static List<Point> moveAllPointsRightBy(List<Point> points, int x) {
return points.stream()
.map(p -> new Point(p.getX() + x, p.getY()))
.collect(toList());
}
위 코드에 람다 표현식 p -> new Point(p.getX() + x, p.getY())을 테스트하는 부분은 없다.