[JAVA] 5. 함수형 프로그래밍 응용 및 디자인 패턴

초보개발자·2024년 1월 25일
0

Java

목록 보기
5/5
post-thumbnail

📕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) {
    // returnTrue만 실행 됨
    if (returnTrue() || returnFalse()) {
        System.out.println("true");
    }
	// 둘다 실행
    if (or(returnTrue(), returnFalse())) {
        System.out.println("true");
    }
	// returnTrue만 실행
    if (lazyOr(() -> returnTrue(), () -> returnFalse())) {
        System.out.println("true");
    }

    // stream은 lazy evaluation을 한다
    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);
    // collect와 같은 종결처리가 이루어 지기 전까지 계산을 미뤄 peek보다 먼저 실행된다
    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.identity()는 자기 자신이 나오는 항등함수라 영향을 주지 않기에 시작점으로 지정
    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)
  1. 생성 패턴 (Creational Patterns)
    – 오브젝트의 생성에 관련된 패턴
  2. 구조 패턴 (Structural Patterns)
    – 상속을 이용해 클래스/오브젝트를 조합하여 더 발전된 구조로 만드는 패턴
  3. 행동 패턴 (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;
}

// User 내부에 생성
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;
	}
	
	// consumer를 통해서 다양한 필드값을 넣어줄 수 있다
    public Builder with(Consumer<Builder> consumer) {
		consumer.accept(this);
		return this;
	}
	
	public User build() {
		return new User(this);
	}
}

// builder를 사용할 때
User user = User.builder(1, "Alice")
		.with(builder -> {
			builder.emailAddress = "alice@fastcampus.co.kr";
			builder.isVerified = true;
		}).build();
  • 대표적인 생성 패턴
  • 객체의 생성에 대한 로직과 표현에 대한 로직을 분리해준다
  • 객체의 생성 과정을 유연하게 해준다
  • 객체의 생성 과정을 정의하고 싶거나 필드가 많아 constructor가 복잡해질 때 유용

3. Decorator Pattern

// default 메소드를 통해 다음 프로세스를 합성 (Function의 andThen 메소드)
@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");
	}

}

// 추가적인 process들을 클래스로 만들어 사용
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);
	}
}

// 클래스로 따로 만든 provider 세팅
emailSender.setEmailProvider(verifyYourEmailAddressEmailProvider);
users.stream()
	.filter(user -> !user.isVerified())
	.forEach(emailSender::sendEmail);

// 람다를 통한 provider 세팅
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

// abstract을 통해 뼈대를 만들어주고
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");
	}
	

}

// case2 : 무분별한 클래스 생성을 막기위한 함수형 인터페이스 사용
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");
		}
	}
}

// case1
UserService userService = new UserService();
userService.createUser(alice);

// case2
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);
  • 책임연쇄 패턴
  • 행동 패턴의 하나
  • 명령과 명령을 각각의 방법으로 처리할 수 있는 처리 객체들이 있을 때, 처리 객체들을 체인으로 엮는다
  • 명령을 처리 객체들이 체인의 앞에서부터 하나씩 처리해보도록 한다
  • 각 처리 객체는 자신이 처리할 수 없을 때 체인의 다음 처리 객체로 명령을 넘긴다
  • 체인의 끝에 다다르면 처리가 끝난다
  • 새로운 처리 객체를 추가하는 것으로 매우 간단히 처리 방법을 더할 수 있다
profile
꾸준히 빠르게

0개의 댓글