📕Scope Closure Curry
1. Scope
public static Supplier<String> getStringSupplier() {
String hello = "Hello";
Supplier<String> supplier = () -> {
String world = "World";
return hello + world;
};
return supplier;
}
- Scope (스코프 / 유효범위) – 변수에 접근할 수 있는 범위
- 함수 안에 함수가 있을 때 내부 함수에서 외부 함수에 있는 변수에 접근이 가능하다 (lexical scope). 그 반대는 불가능하다.
2. Closure
- 내부 함수가 존재하는 한 내부 함수가 사용한 외부 함수의 변수들 역시 계속 존재한다. 이렇게 lexical scope를 포함하는 함수를 closure라 한다.
- 이 때 내부 함수가 사용한 외부 함수의 변수들은 내부 함수 선언 당시로부터 변할 수 없기 때문에 final로 선언되지 않더라도 암묵적으로 final로 취급된다.
3. Curry
BiFunction<Integer, Integer, Integer> add = (x, y) -> x + y;
=>
Function<Integer, Function<Integer, Integer>> add =
x -> y -> x + y;
- 여러 개의 매개변수를 받는 함수를 중첩된 여러 개의 함수로 쪼개어 매개 변수를 한 번에 받지 않고 여러 단계에 걸쳐 나눠 받을 수 있게 하는 기술
- Closure의 응용
📓Lazy Evaluation
public static void main(String[] args) {
if (returnTrue() || returnFalse()) {
System.out.println("true");
}
if (or(returnTrue(), returnFalse())) {
System.out.println("true");
}
if (lazyOr(() -> returnTrue(), () -> returnFalse())) {
System.out.println("true");
}
Stream<Integer> integerStream = Stream.of(3, -2, 5, 8, -3, 10)
.filter(x -> x > 0)
.peek(x -> System.out.println("peeking " + x))
.filter(x -> x % 2 ==0);
System.out.println("Before collect");
List<Integer> integers = integerStream.collect(Collectors.toList());
System.out.println("After collect: " + integers);
}
public static boolean or(boolean x, boolean y) {
return x || y;
}
public static boolean lazyOr(Supplier<Boolean> x, Supplier<Boolean> y) {
return x.get() || y.get();
}
public static boolean returnTrue() {
System.out.println("Returning true");
return true;
}
public static boolean returnFalse() {
System.out.println("Returning false");
return false;
}
- Lambda의 계산은 그 결과값이 필요할 때가 되어서야 계산된다
- 이를 이용하여 불필요한 계산을 줄이거나 해당 코드의 실행 순서를 의도적으로 미룰 수 있다
📗Function Composition(함수 합성)
java.util.function.Function
<V> Function<V, R> compose(Function<? super V, ? extends T> before)
<V> Function<T, V> andThen(Function<? super R, ? extends V> after)
public static void main(String[] args) {
Function<Integer, Integer> multiplyByTwo = x -> 2 * x;
Function<Integer, Integer> addTen = x -> x + 10;
Function<Integer, Integer> composedFunction = multiplyByTwo.andThen(addTen);
System.out.println(composedFunction.apply(3));
Order unprocessedOrder = new Order()
.setId(1001L)
.setOrderLines(Arrays.asList(
new OrderLine().setAmount(BigDecimal.valueOf(1000)),
new OrderLine().setAmount(BigDecimal.valueOf(2000))));
List<Function<Order, Order>> priceProcessors = getPriceProcessors(unprocessedOrder);
Function<Order, Order> mergedPriceProcessors = priceProcessors.stream()
.reduce(Function.identity(), Function::andThen);
Order processedOrder = mergedPriceProcessors.apply(unprocessedOrder);
System.out.println(processedOrder);
}
public static List<Function<Order, Order>> getPriceProcessors(Order order) {
return Arrays.asList(new OrderLineAggregationPriceProcessor(),
new TaxPriceProcessor(new BigDecimal("9.375")));
}
- 여러 개의 함수를 합쳐 하나의 새로운 함수로 만드는 것
📑Design Pattern
1. 디자인 패턴이란
- 반복해서 등장하는 프로그래밍 문제들에 대한 해법들을 패턴화 해놓은 것
- 패턴들을 숙지해놓으면 비슷한 문제가 생겼을 때 패턴들이 이정표가 되어준다
- 대부분의 디자인 패턴들은 구현에 많은 인터페이스/클래스/메서드를 필요로 한다
- 함수형 프로그래밍을 이용해 몇몇 패턴을 좀 더 간단하게 구현할 수 있다
- < Design Patterns> by Gang of Four (GoF)
- 생성 패턴 (Creational Patterns)
– 오브젝트의 생성에 관련된 패턴
- 구조 패턴 (Structural Patterns)
– 상속을 이용해 클래스/오브젝트를 조합하여 더 발전된 구조로 만드는 패턴
- 행동 패턴 (Behavioral Patterns)
– 필요한 작업을 여러 객체에 분배하여 객체간 결합도를 줄이게 해주는 패턴
2. Builder Pattern
public User(Builder builder) {
this.id = builder.id;
this.name = builder.name;
this.emailAddress = builder.emailAddress;
this.isVerified = builder.isVerified;
this.createdAt = builder.createdAt;
this.friendUserIds = builder.friendUserIds;
}
public static class Builder {
private int id;
private String name;
public String emailAddress;
public boolean isVerified;
public LocalDateTime createdAt;
public List<Integer> friendUserIds = new ArrayList<>();
private Builder(int id, String name) {
this.id = id;
this.name = name;
}
public Builder with(Consumer<Builder> consumer) {
consumer.accept(this);
return this;
}
public User build() {
return new User(this);
}
}
User user = User.builder(1, "Alice")
.with(builder -> {
builder.emailAddress = "alice@fastcampus.co.kr";
builder.isVerified = true;
}).build();
- 대표적인 생성 패턴
- 객체의 생성에 대한 로직과 표현에 대한 로직을 분리해준다
- 객체의 생성 과정을 유연하게 해준다
- 객체의 생성 과정을 정의하고 싶거나 필드가 많아 constructor가 복잡해질 때 유용
3. Decorator Pattern
@FunctionalInterface
public interface PriceProcessor {
Price process(Price price);
default PriceProcessor andThen(PriceProcessor next) {
return price -> next.process(process(price));
}
}
public class DiscountPriceProcessor implements PriceProcessor {
@Override
public Price process(Price price) {
return new Price(price.getPrice() + ", then applied discount");
}
}
PriceProcessor decoratedPriceProcessor = basicPriceProcessor
.andThen(discountPriceProcessor)
.andThen(taxPriceProcessor);
Price processedPrice = decoratedPriceProcessor.process(unprocessedPrice);
System.out.println(processedPrice.getPrice());
PriceProcessor decoratedPriceProcessor2 = basicPriceProcessor
.andThen(taxPriceProcessor)
.andThen(price -> new Price(price.getPrice() + ", then apply another procedure"));
Price processedPrice2 = decoratedPriceProcessor2.process(unprocessedPrice);
System.out.println(processedPrice2.getPrice());
- 구조 패턴의 하나
- 용도에 따라 객체에 기능을 계속 추가(decorate)할 수 있게 해준다
4. Strategy Pattern
public class VerifyYourEmailAddressEmailProvider implements EmailProvider {
@Override
public String getEmail(User user) {
return "'Verify Your Email Address' email for " + user.getName();
}
}
public class EmailSender {
private EmailProvider emailProvider;
public EmailSender setEmailProvider(EmailProvider emailProvider) {
this.emailProvider = emailProvider;
return this;
}
public void sendEmail(User user) {
String email = emailProvider.getEmail(user);
System.out.println("Sending " + email);
}
}
emailSender.setEmailProvider(verifyYourEmailAddressEmailProvider);
users.stream()
.filter(user -> !user.isVerified())
.forEach(emailSender::sendEmail);
emailSender.setEmailProvider(user -> "'Play With Friends' email for " + user.getName());
users.stream()
.filter(User::isVerified)
.filter(user -> user.getFriendUserIds().size() > 5)
.forEach(emailSender::sendEmail);
- 대표적인 행동 패턴
- 런타임에 어떤 전략(알고리즘)을 사용할 지 선택할 수 있게 해준다
- 전략들을 캡슐화 하여 간단하게 교체할 수 있게 해준다
5. Template Method Pattern
public abstract class AbstractUserService {
protected abstract boolean validateUser(User user);
protected abstract void writeToDB(User user);
public void createUser(User user) {
if (validateUser(user)) {
writeToDB(user);
} else {
System.out.println("Cannot create user");
}
}
}
public class UserService extends AbstractUserService {
@Override
protected boolean validateUser(User user) {
System.out.println("Validating user " + user.getName());
return user.getName() != null && user.getEmailAddress().isPresent();
}
@Override
protected void writeToDB(User user) {
System.out.println("Writing user " + user.getName() + " to DB");
}
}
public class UserServiceInFunctionalWay {
private final Predicate<User> validateUser;
private final Consumer<User> writeToDB;
public UserServiceInFunctionalWay(Predicate<User> validateUser, Consumer<User> writeToDB) {
this.validateUser = validateUser;
this.writeToDB = writeToDB;
}
public void createUser(User user) {
if (validateUser.test(user)) {
writeToDB.accept(user);
} else {
System.out.println("Cannot create user");
}
}
}
UserService userService = new UserService();
userService.createUser(alice);
UserServiceInFunctionalWay userServiceInFunctionalWay = new UserServiceInFunctionalWay(
user -> {
System.out.println("Validating user " + user.getName());
return user.getName() != null && user.getEmailAddress().isPresent();
},
user -> {
System.out.println("Writing user " + user.getName() + " to DB");
});
userServiceInFunctionalWay.createUser(alice);
- 또 하나의 대표적인 행동 패턴
- 상위 클래스는 알고리즘의 뼈대만을 정의하고 알고리즘의 각 단계는 하위 클래스에게 정의를 위임하는 패턴
- 알고리즘의 구조를 변경하지 않고 세부 단계들을 유연하게 변경할 수 있게 해준다
6. Chain of Responsibility Pattern
// step 설정하는 클래스
public class OrderProcessStep {
private final Consumer<Order> processOrder;
private OrderProcessStep next;
public OrderProcessStep(Consumer<Order> processOrder) {
this.processOrder = processOrder;
}
public OrderProcessStep setNext(OrderProcessStep next) {
if (this.next == null) {
this.next = next;
} else {
this.next.setNext(next);
}
return this;
}
public void process(Order order) {
processOrder.accept(order);
Optional.ofNullable(next)
.ifPresent(nextStep -> nextStep.process(order));
}
}
// case 1
OrderProcessStep verifyOrderStep = new OrderProcessStep(order -> {
if (order.getStatus() == OrderStatus.IN_PROGRESS) {
System.out.println("Verifying order " + order.getId());
if (order.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
order.setStatus(OrderStatus.ERROR);
}
}
});
// case 2
OrderProcessStep processPaymentStep = new OrderProcessStep(order -> {
if (order.getStatus() == OrderStatus.IN_PROGRESS) {
System.out.println("Processing payment of order " + order.getId());
order.setStatus(OrderStatus.PROCESSED);
}
});
// case 3
OrderProcessStep handleErrorStep = new OrderProcessStep(order -> {
if (order.getStatus() == OrderStatus.ERROR) {
System.out.println("Sending out 'Failed to process order' alert for order " + order.getId());
}
});
// 각 스텝 설정
OrderProcessStep chainedOrderProcessSteps = initializeStep
.setNext(setOrderAmountStep)
.setNext(verifyOrderStep)
.setNext(processPaymentStep)
.setNext(handleErrorStep)
.setNext(completeProcessingOrderStep);
// 스텝실행(진행도중 처리할 수 없다면 스킵)
chainedOrderProcessSteps.process(order);
- 책임연쇄 패턴
- 행동 패턴의 하나
- 명령과 명령을 각각의 방법으로 처리할 수 있는 처리 객체들이 있을 때, 처리 객체들을 체인으로 엮는다
- 명령을 처리 객체들이 체인의 앞에서부터 하나씩 처리해보도록 한다
- 각 처리 객체는 자신이 처리할 수 없을 때 체인의 다음 처리 객체로 명령을 넘긴다
- 체인의 끝에 다다르면 처리가 끝난다
- 새로운 처리 객체를 추가하는 것으로 매우 간단히 처리 방법을 더할 수 있다