design
- OOP와 함수형 스타일 시너지
- 설계 아이디어 변화
- 객체 대신에 경량의 함수형 스타일, 람다식을 사용한 로직과 함수 분리
- 델리게이트 delegate > 전략패턴 stratage, 데코레이터 패턴 decorator
* seperate of concerns 문제, 관심사, 걱정거리의 분리
- 기본 로직, 예외 처리, 비동기 프로그래밍, 메모리 할당/해제, 인자 전달 방식, 락(lock), 이벤트 > 인간의 인지능력 한계
- 인지 심리학자인 George A. Miller은 The Magical Number Seven, Plus or Minus Two 한 번에 처리할 수 있는 정보가 7개 정도(혹은 1,2개 더)로 제한
- 어렵고 복잡 > 걱정거리들을 분리, 작은 문제로 나누기 - 디자인패턴, OOP 그리고 람다!
관심사의 분리 - 메소드 레벨
- 중복, copy&paste
//자산 ASSET 처리하기
public static int totalAssetValues(final List<Asset> assets) {
return assets.stream().mapToInt(Asset::getValue).sum();
}
//채권 BOND 만 처리하기
public static int totalBondValues(final List<Asset> assets) {
return assets.stream().mapToInt(asset -> asset.getType() == AssetType.BOND ? asset.getValue() : 0).sum();
}
//주식 STOCK 만 처리하기
public static int totalStockValues(final List<Asset> assets) {
return assets.stream().mapToInt(asset -> asset.getType() == AssetType.STOCK ? asset.getValue() : 0).sum();
}
// 람다식을 제외한 중복 - 이터레이션, 합계
// mapToInt()에 들어가는 람다식만 다르다.
// 변하지 않는 부분과 변하는 부분, 변하는 부분을 관심사로 지정 > OOP 전략패턴 적용 > 람다식으로 구현
public static void main(final String[] args) {
final List<Asset> assets = Arrays.asList(
new Asset(Asset.AssetType.BOND, 1000),
new Asset(Asset.AssetType.BOND, 2000),
new Asset(Asset.AssetType.STOCK, 3000),
new Asset(Asset.AssetType.STOCK, 4000)
);
System.out.println("Total of all assets: " + totalAssetValues(assets));
System.out.println("Total of bonds: " + totalBondValues(assets));
System.out.println("Total of stocks: " + totalStockValues(assets));
}
- 관심사의 분리, 재사용성, 경량(객체불필요)
//람다식을 적용한 메소드(고차함수) 추가, 파라미터 Predicate 함수형 인터페이스 재사용, 람다식을 전달 받을 수 있다.
//람다식 > 변하는 부분, 고차함수 > 안변하는 부분 , 람다식을 사용한 전략패턴
//별도 객체 불필요
//open/closed principal 적용
public static int totalAssetValues(final List<Asset> assets, final Predicate<Asset> assetSelector) {
return assets.stream().filter(assetSelector).mapToInt(Asset::getValue).sum();
}
...
System.out.println("Total of all assets: " + totalAssetValues(assets, asset -> true));
System.out.println("Total of bonds: " + totalAssetValues(assets, asset -> asset.getType() == AssetType.BOND));
System.out.println("Total of stocks: " + totalAssetValues(assets, asset -> asset.getType() == AssetType.STOCK));
관심사의분리 두번째 - 클래스 레벨
- 책임져야 할 부분을 다른 클레스 보다 람다식(메서드 레퍼런스)으로 델리게이트delegation
- 클래스의 증가를 막아줌
- 주식 값을 리턴하는 객체를 사용한 예제
//스트링 값을 받으면 값을 리턴하는 함수형 인터페이스
//주식명을 입력하면 가격을 리턴
private Function<String, BigDecimal> priceFinder;
public BigDecimal computeStockWorth(final String ticker, final int shares) {
return priceFinder.apply(ticker).multiply(BigDecimal.valueOf(shares));
}
// ... other methods that use the priceFinder ...
//생성자, 함수형 인터페이스인 Function - priceFinder를 입력받는다.
//Junit에서 간단한 람다식으로 입력 final CalculateNAV calculateNAV = new CalculateNAV(ticker -> new BigDecimal("6.01"));
//정적 메소드 레퍼런스 형태로 입력 final CalculateNAV calculateNav = new CalculateNAV(YahooFinance::getPrice);
public CalculateNAV(final Function<String, BigDecimal> aPriceFinder) {
priceFinder = aPriceFinder;
}
public static void main(String[] args) {
//생성자 인젝션, DI
//YahooFinance::getPrice는 야후에 웹서비스 방식으로 주식에 대한 가격을 리턴받는 정적 메소드
final CalculateNAV calculateNav = new CalculateNAV(YahooFinance::getPrice); //정적 메소드 레퍼런스 형태로 입력
System.out.println(String.format("100 shares of Google worth: $%.2f", calculateNav.computeStockWorth("GOOG", 100)));
}
}
데코레이팅 decorating
- 행위(비헤이비어, 람다식)를 여러개를 체인으로 묶어서 사용
- 데코레이터 패턴은 클래스와 인터페이스 계층이 복잡 > 덜 부담
람다식을 사용하여 델리게이트를 조함
카메라 필터(여러개)를 추가하는 예제
@SuppressWarnings("unchecked")
public class Camera {
private Function<Color, Color> filter;
public Color capture(final Color inputColor) {
final Color processedColor = filter.apply(inputColor);
// ... more processing of color...
return processedColor;
}
//filter(람다식)를 여러개 입력하면 체인으로 하나로 연결한다.
//여러 필터를 받아서 처리하도록 확장성이 높아짐
//디폴트 메소드인 compose가 핵심코드이다. 여러 Function을 조합
public void setFilters(final Function<Color, Color>... filters) {
//필터가 없으면 reduce에서 리턴인 Optional이 empty를 리턴
//orElse(color -> color); > .orElseGet(Function::identity); 메소드 레퍼런스로 변환가능
filter = Stream.of(filters).reduce((filter, next) -> filter.compose(next)).orElse(color -> color);
}
public Camera() {
setFilters();
}
public static void main(final String[] args) {
final Camera camera = new Camera();
// 단순한 화면 출력 용도로 함수형인터페이스(Consumer)를 사용
final Consumer<String> printCaptured = (filterInfo) ->
System.out.println(String.format("with %s: %s", filterInfo, camera.capture(new Color(200, 100, 200))));
//printCaptured함수를 accept 메소드로 실행함
//필터없음
printCaptured.accept("no filter");
camera.setFilters(Color::brighter);
printCaptured.accept("brighter filter");
camera.setFilters(Color::darker);
printCaptured.accept("darker filter");
//2개의 필터가 하나로 연결된다.
camera.setFilters(Color::brighter, Color::darker);
printCaptured.accept("brighter & darker filter");
}
외부 인터페이스 만들기
람다식을 활용한 외부 인터페이스 만들기
기존 방법
//메일 송신 객체 API를 사용
public class Mailer {
public void from(final String address) {/* ... */ }
public void to(final String address) {/* ... */ }
public void subject(final String line) {/* ... */ }
public void body(final String message) {/* ... */ }
public void send() {System.out.println("sending...");
}
// 메일 송신 객체 API를 통한 생성
// 반복 mailer 레퍼런스, 모호성 send()으로 인해 API를 꼼꼼이 살펴야 함 > 직관성 필요
// 명확하지 않는 객체의 라이프타임
Mailer mailer = new Mailer();
mailer.from("[email protected]");
mailer.to("[email protected]");
mailer.subject("build notification");
mailer.body("...your code sucks...");
mailer.send();
- 메서드 체인 method chaining, 캐스케이드 메서드 패턴 cascade method pattern
//void 대신에 this를 리턴한다.
public MailBuilder from(final String address) { /*... */; return this; }
public MailBuilder to(final String address) { /*... */; return this; }
public MailBuilder subject(final String line) { /*... */; return this; }
public MailBuilder body(final String message) { /*... */; return this; }
public void send() { System.out.println("sending..."); }
// 메소드 체이닝 패턴으로 작성한 코드
public static void main(final String[] args) {
new MailBuilder() // new 키워드가 눈에 가시...
.from("[email protected]") //한 번씩만 호출됨을 보장했으면...
.to("[email protected]")
.subject("build notification")
.body("...it sucks less...")
.send();
}
- 람다식을 활용
//생성자를 통한 직접 생성 불가로 제한
private FluentMailer() {}
//메소드 체이닝 패턴 유지
public FluentMailer from(final String address) { /*... */; return this; }
public FluentMailer to(final String address) { /*... */; return this; }
public FluentMailer subject(final String line) { /*... */; return this; }
public FluentMailer body(final String message) { /*... */; return this; }
// FluentMailer의 생성을 send 메소드 안에서 관리함 > 내부에서 인스턴스를 제어함
// 함수형 인터페이스 Consumer를 사용
public static void send(final Consumer<FluentMailer> block) {
//인스턴스의 생성 > 리턴 후 gc됨 > 론 패턴 loan pattern (객체 라이프타임 관리)
final FluentMailer mailer = new FluentMailer();
block.accept(mailer); // 람다식을 수행
System.out.println("sending...");
}
...
//사용자는 인스턴스 생성을 직접 하지 않음
// 람다식(코드 블록)을 넘김
FluentMailer.send(mailer -> mailer.from("[email protected]")
.to("[email protected]")
.subject("build notification")
.body("...much better...")
);
}
}
정리
- 람다식은 경량화 설계툴
- 많고 복잡한 인터페이스와 클래스 구조 > 람다식/함수형 인터페이스로 경량화
- 쉬운 전략 패턴, 데코레이터 패턴 구현
- 단, 람다식에서의 예외 처리 주의!