리팩토링 / 테스팅 / 디버깅
- 람다 표현식으로 코드 리펙토링하기
- 람다 표현식으로 객체지향 설계 패턴에 미치는 영향
- 람다 표현식 테스팅
- 람다 표현식과 스트림 API 사용 코드 디버깅
- 람다표현식으로 전략 / 템플릿 메서드 / 옵저버 / 의무 체인 / 팩토리 등의 객체 지향 디자인 패턴을 어떻게 간소화할 수 있는지 살펴보기.
코드 가독성이 좋다는 것은 추상적인 표현이므로 이를 정확하게 정의하기 어렵다. 일반적으로 코드 가독성이 좋다는 것은 '어떤 코드를 다른 사람도 쉽게 이해할 수 있음'을 의미, 즉 코드 가독성을 개선한다는 것은 우리가 구현한 코드를 다른 사람이 쉽게 이해하고 유지보수 할 수 있게 만드는 것.
1. 익명 클래스를 람다 표현식으로 리팩토링 하기
Runnable r1 = new Runnable(){
public void run(){
System.out.println("Hello");
}
}
Runnable r2 = () -> System.out.println("Hello");
익명 클래스에서 사용한 this와 super는 람다 표현식에서 다른 의미를 갖음.
익명 클래스에서 this는 익명클래스 자신을 가리키지만 람다에서 this는 람다를 감싸는 클래스를 가리킨다.
익명 클래스를 감싸고 있는 클래스의 변수를 가릴 수 있다(shadow variable) 하지만 람다 표현식에서는 변수를 가릴 수 없다.
int a = 10;
Runnable r1 = () -> {
int a = 2;
System.out.println("Hello");
};
//위 코드는 동작하지 않음
익명 클래스를 람다 표현식로 바꾸면 메서드를 호출할 때 Runnable과 Task 모두 대상 형식이 될 수 있으므로 모호함 발생
doSomething(() -> System.out.println("Hello"));
이럴 때는 명시적 형변환(Task) 를 이용해 모호함을 제거
doSomething((Task)() -> System.out.println("Hello"));
2. 람다 표현식을 메서드 레퍼런스로 리팩토링 하기
메서드 레퍼런스를 활용하면서 comparing과 maxBy와 같은 정적 헬퍼 메서드
를 활용하는 것이 좋다.
inventory.sort(
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
를 다음과 같이 수정한다.
inventory.sort(comparing(Apple::getWeight));
최댓값이나 합계를 계산할 때 람다 표현식과 저수준 리듀싱 연산을 조합하는 것보다 Collectors API를 사용하면 코드의 의도가 더 명확해진다.
int totalCalories = menu.stream().map(Dish::getCalories)
.reduce(0, (c1, c2) -> c1 + c2);
를 다음과 같이 수정한다.
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
01. 코드 유연성 개선하기.
함수형 인터페이스 적용
람다 표현식을 이용하려면 함수형 인터페이스가 필요합니다. 따라서 함수형 인터페이스를 코드에 추가해야 하는데, 이 때 사용될 수 패턴으로
- 조건부 연기 실행 (Conditional deferred execution)
- 실행 어라운드 (execute around)
즉, 두 가지 자주 사용하는 패턴이 있다.
조건부 연기 실행
if(logger.isLoggable(Log.FINGER)){
logger.finger("Problem: " + generate());
}
이런 코드가 있다고 하자. 이런 코드의 문제점은?
- logger 의 상태가 isLoggable이라는 메서드에 의해 클라이언트 코드로 노출
- 메시지를 로깅할 때마다 logger 객체의 상태를 매번 확인해야 할까? 코드가 지저분
그러면?
logger.log(Level.FINER, "Problem: " + generate())
이런 코드가 정답일까??
람다 표현식을 사용하면
특정 조건에서만 메시지가 생성될 수 있도록 메시지 생성 과정을 연기(defer) 해야 한다. 자바 8에서는 logger문제를 해결할 수 있도록 Supplier를 인수로 갖는 오버로드된 log메서드를 제공 새로 추가된 log 메서드의 시그니처이다.
public void log(Level level, Supplier<String> msgSupplier){
if(logger.isLoggable(level)){
log(level, msgSupplier.ger)
}
}
logger.log(Level.FINER, () -> "Problem " + generate()) 로 변경 될 수 있다.
위 문제점이였던 클라 코드의 노출을 막고, 가독성이 높아진다.
실행 어라운드
매번 같은 준비, 종료 과정을 반복적으로 수행하는 코드가 있다면 이를 람다로 변환할 수 있다.
String oneLine = processFile((BufferedReader b) -> b.readLine());
String twoLine = processFile((BufferedReader b) -> b.readLine() + b.readLine());
public static String processFile(BufferedReaderProcessor p) throws IOException {
try(BufferedReader br = new BufferedReader(new FileReader("java.txt"))){
return p.process(br); //인수로 전달된 BufferedReaderProcessor를 실행
}
}
// IOException을 던질 수 있는 람다의 함수형 인터페이스
public interface BufferedReaderProcessor {
String process(BufferedReader br) throws IOException;
}
02. 객체지향 디자인 패턴 리펙토링 하기.
- 전략 패턴
- 템플릿 메서드
- 옵저버
- 의미 체인
- 팩토리
전략 패턴
Predicate 를 활용해서 전략 패턴 구현
public class StrategyMain {
public static void main(String[] args) {
// old school
Validator v1 = new Validator(new IsNumeric());
System.out.println(v1.validate("aaaa"));
Validator v2 = new Validator(new IsAllLowerCase ());
System.out.println(v2.validate("bbbb"));
// with lambdas
Validator v3 = new Validator((String s) -> s.matches("\\d+"));
System.out.println(v3.validate("aaaa"));
Validator v4 = new Validator((String s) -> s.matches("[a-z]+"));
System.out.println(v4.validate("bbbb"));
}
interface ValidationStrategy {
public boolean execute(String s);
}
static private class IsAllLowerCase implements ValidationStrategy {
public boolean execute(String s){
return s.matches("[a-z]+");
}
}
static private class IsNumeric implements ValidationStrategy {
public boolean execute(String s){
return s.matches("\\d+");
}
}
static private class Validator{
private final ValidationStrategy strategy;
public Validator(ValidationStrategy v){
this.strategy = v;
}
public boolean validate(String s){
return strategy.execute(s); }
}
}
템플릿 메서드
알리즘의 개요를 제시한 다음에 일부를 고칠 수 있는 유연함을 제공해야 할 때 템플릿 메서드 디자인 패턴을 사용한다. 다시말해, 템플릿 메서드는 '이 알고리즘을 사용하고 싶은데 그대로는 안 되고 조금 고쳐야 하는' 상황에 적합하다.
이전에는
package chap8;
abstract class OnlineBanking {
public void processCustomer(int id){
Customer c = Database.getCustomerWithId(id);
makeCustomerHappy(c);
}
abstract void makeCustomerHappy(Customer c);
// dummy Customer class
static private class Customer {}
// dummy Datbase class
static private class Database{
static Customer getCustomerWithId(int id){ return new Customer();}
}
}
// 그 뒤에 OnlineBaning을 상속받아 makeCustomerHappy를 구현해야 한다.
그러나 람다 표현식을 활용하게 된다면,
import java.util.function.Consumer;
public class OnlineBankingLambda {
public static void main(String[] args) {
new OnlineBankingLambda().processCustomer(1337, (Customer c) -> System.out.println("Hello!"));
}
public void processCustomer(int id, Consumer<Customer> makeCustomerHappy){
Customer c = Database.getCustomerWithId(id);
makeCustomerHappy.accept(c);
}
// dummy Customer class
static private class Customer {}
// dummy Database class
static private class Database{
static Customer getCustomerWithId(int id){ return new Customer();}
}
}
와 같이 상속을 받지 않고 구현할 수 있다.
옵저버
어떤 이벤트가 발생 했을 때 한 객체(주제- subject라 불리는) 가 다른 객체 리스트(옵저버 라 불리는)에 자동으로 알림을 보내야 하는 상황에서 옵저버 디자인 패턴을 사용
import java.util.ArrayList;
import java.util.List;
public class ObserverMain {
public static void main(String[] args) {
Feed f = new Feed();
f.registerObserver(new NYTimes());
f.registerObserver(new Guardian());
f.registerObserver(new LeMonde());
f.notifyObservers("The queen said her favourite book is Java 8 in Action!");
Feed feedLambda = new Feed();
feedLambda.registerObserver((String tweet) -> {
if(tweet != null && tweet.contains("money")){
System.out.println("Breaking news in NY! " + tweet); }
});
feedLambda.registerObserver((String tweet) -> {
if(tweet != null && tweet.contains("queen")){
System.out.println("Yet another news in London... " + tweet); }
});
feedLambda.notifyObservers("Money money money, give me money!");
}
interface Observer{
void inform(String tweet);
}
interface Subject{
void registerObserver(Observer o);
void notifyObservers(String tweet);
}
static private class NYTimes implements Observer{
@Override
public void inform(String tweet) {
if(tweet != null && tweet.contains("money")){
System.out.println("Breaking news in NY!" + tweet);
}
}
}
static private class Guardian implements Observer{
@Override
public void inform(String tweet) {
if(tweet != null && tweet.contains("queen")){
System.out.println("Yet another news in London... " + tweet);
}
}
}
static private class LeMonde implements Observer{
@Override
public void inform(String tweet) {
if(tweet != null && tweet.contains("wine")){
System.out.println("Today cheese, wine and news! " + tweet);
}
}
}
static private 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.inform(tweet));
}
}
}
의무 체인
작ㅂ 처리 객체의 체인(동작 체인 등)을 만들 때는 의무 체인 패턴을 사용한다. 한 객체가 어떤 작업을 처리한 다음에 다른 객체로 결과를 전달하고, 다른 객체도 해야 할 작업을 처리한 다음에 또 다른 객체로 전달하는 식이다.
import java.util.function.Function;
import java.util.function.UnaryOperator;
public class ChainOfResponsibilityMain {
public static void main(String[] args) {
ProcessingObject<String> p1 = new HeaderTextProcessing();
ProcessingObject<String> p2 = new SpellCheckerProcessing();
p1.setSuccessor(p2);
String result1 = p1.handle("Aren't labdas really sexy?!!");
System.out.println(result1);
UnaryOperator<String> headerProcessing =
(String text) -> "From Raoul, Mario and Alan: " + text;
UnaryOperator<String> spellCheckerProcessing =
(String text) -> text.replaceAll("labda", "lambda");
Function<String, String> pipeline = headerProcessing.andThen(spellCheckerProcessing);
String result2 = pipeline.apply("Aren't labdas really sexy?!!");
System.out.println(result2);
}
static private 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);
}
return r;
}
abstract protected T handleWork(T input);
}
static private class HeaderTextProcessing
extends ProcessingObject<String> {
public String handleWork(String text) {
return "From Raoul, Mario and Alan: " + text;
}
}
static private class SpellCheckerProcessing
extends ProcessingObject<String> {
public String handleWork(String text) {
return text.replaceAll("labda", "lambda");
}
}
}
팩토리
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
public class FactoryMain {
public static void main(String[] args) {
Product p1 = ProductFactory.createProduct("loan");
Supplier<Product> loanSupplier = Loan::new;
Product p2 = loanSupplier.get();
Product p3 = ProductFactory.createProductLambda("loan");
}
static private 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 new RuntimeException("No such product " + name);
}
}
public static Product createProductLambda(String name){
Supplier<Product> p = map.get(name);
if(p != null) return p.get();
throw new RuntimeException("No such product " + name);
}
}
static private interface Product {}
static private class Loan implements Product {}
static private class Stock implements Product {}
static private class Bond implements Product {}
final static private Map<String, Supplier<Product>> map = new HashMap<>();
static {
map.put("loan", Loan::new);
map.put("stock", Stock::new);
map.put("bond", Bond::new);
}
}
정보 로깅
peek
이라는 스트림 연산을 활용. peek 은 스트림의 각 요소를 소비한 것처럼 동작을 실행한다. 하지만 forEach처럼 실제로 스트림의 요소를 소비하지는 않는다.
'Programming Language 이해하기 > Java 이해하기' 카테고리의 다른 글
자바8 inAction - [10] Null 대신 Optioanl (0) | 2019.08.31 |
---|---|
자바8 inAction - [9] Default method 이해하기 (0) | 2019.08.31 |
자바8 inAction - [7] 병렬데이터 처리와 성능 (0) | 2019.08.31 |
자바8 inAction - [6] 스트림으로 데이터 수집하기 (0) | 2019.08.31 |
자바8 inAction - [5] 스트림 활용 - 스트림 제대로 알고 사용하기. (0) | 2019.08.31 |
댓글