Justin의 개발 로그

DSL이 파라미터의 위치에 따라 역할이 정해지는 중첩된 함수(Nested Function) 패턴으로 구현된 DSL의 단점은 Lambda(함수형인터페이스)를 활용해 개선할 수 있다. 

 

NestedFunction + Lambda를 활용한 파라미터 역할 지정

기본형 파라미터로 입력받던 quantity와 price를 대체하기 위한 Supplier를 정의

public class SimpleSupplier<T> implements Supplier<T> {
    final T value;

    protected SimpleSupplier(final T value) {
        this.value = value;
    }

    private SimpleSupplier() {
        value = null;
    }

    @Override
    public T get() {
        return value;
    }
}

Quantity와 Price 클래스 생성

public class Quantity<Integer> extends SimpleSupplier {
    protected Quantity(Integer value) {
        super(value);
    }
}
public class Price<Double> extends SimpleSupplier {
    protected Price(Double value) {
        super(value);
    }
}
  • 객체를 직접 생성하는 가능성을 없애기 위해 생성자를 Protected로 외부에서 생성 차단
  • Quantity<Integer>, Price<Double>의 중복되는 코드를 부모클래스(SimpleSupplier)의 제네릭<T>를 활용해 정리
public class NestedFunctionOrderBuilderWithLambda {
    public static Order order(String customer, Trade... trades) {
        Order order = new Order();
        order.setCustomer(customer);
        Stream.of(trades).forEach(order::addTrade); //주문에 모든 거래 추가
        return order;
    }

    public static Trade buy(Quantity quantity, Stock stock, Price price) {
        return buildTrade(quantity, stock, price, Trade.Type.BUY);
    }

    public static Trade sell(Quantity quantity, Stock stock, Price price) {
        return buildTrade(quantity, stock, price, Trade.Type.SELL);
    }

    private static Trade buildTrade(Supplier<Integer> quantity, Stock stock, Supplier<Double> price, Trade.Type type) {
        Trade trade = new Trade();
        trade.setQuantity(quantity.get());
        trade.setType(type);
        trade.setStock(stock);
        trade.setPrice(price.get());

        return trade;
    }

    public static Stock stock(String symbol, String market) {
        Stock stock = new Stock();
        stock.setSymbol(symbol);
        stock.setMarket(market);
        return stock;
    }

    public static Price<Double> price(Double price) {
        return new Price(price);
    }

    public static Quantity<Integer> quantity(Integer quantity) {
        return new Quantity(quantity);
    }
}
  • buy(), sell()에 사용되던 integer, double 파라미터라 각각 Quantity, Price 로 변경되었다.
  • Quantity, Price는 Supplier<T>를 상속받아 만들었기 때문에 함수형인터페이스이며, 객체가 아닌 메서드 참조로 적용된다.
  • Quantity, Price 클래스의 생성자는 protected로 직접 생성이 불가능하며, 각각 quantity(), price() static 팩토리 메서드를 통해 만들 수 있다. 즉, 파라미터 주입 방식을 한가지로 제한한다.
    • 숫자로 인자를 넣는 방식보다 언어(DSL)이 명확해 진다. 즉, 비개발자인 도메인 전문가도 언어를 이해하기 쉬워진다.
    • 객체를 생성하는 방식을 static 팩토리 메서드로 제한함으로서 의도하지 않은 코드가 생성되는 것을 방지한다.

After 사용 예

    @Test
    void 중첩함수_람다이용_명확화() {
        Order order = order("myCustomer",
			buy(quantity(100), stock("IBM", "NYSE"), price(125.00)),
			sell(quantity(50), stock("GOOGLE", "NASDAQ"), price(375.00))
        );

        System.out.println(order.getValue());
    }

Before 비교

@Test
void 중첩함수이용_간소화() {
    Order order = order("myCustomer",
            buy(80, stock("IBM", "NYSE"), 125.00),
            sell(50, stock("GOOGLE", "NASDAQ"), 375.00)
    );

    System.out.println(order.getValue());
}

 

[Question]

  • Stock은 함수형인터페이스를 사용하지 않는데, Price, Quantity는 왜 복잡하게 함수형 인터페이스를 사용하나?
    • Stock은 객체를 넣는 방식인데, double, integer는 기본형으로 DSL에서 값의 이해가 어려움
    • double, integer를 메서드로 생성해도 결국 입력 파라미터가 기본형으로 받기 때문에 메서드 사용을 강제하지 못함
    • 동일한 리턴값을 갖는 메서드가 여러 개 있는 경우 어느 메서드를 사용해야 하는지 알 수 없음

[장단점]

  • 단점 - 함수형 인터페이스로 메서드 참조를 제공하기 위해 추가되는 클래스와 로직이 늘어남
  • 장점 - 구현이 복잡해지는 대신에 사용 방식이 명확해지는 것이 장점
profile

Justin의 개발 로그

@라이프노트

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!