빌더 패턴
- 대표적인 생성 패턴이다.
- 객체의 생성에 대한 로직과 표현에 대한 로직을 분리해준다.
- 객체의 생성 과정을 유연하게 해준다.
- 객체의 생성 과정을 정의하고 싶거나 필드가 많아 constructor 가 복잡해질 때 유용하다.
- 아래와 같이 빌더 패턴을 구현할 수 있다.
public class User {
private int id;
private String name;
private String emailAddress;
private boolean isVerified;
private LocalDateTime createdAt;
private List<Integer> friendUserIds;
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 int getId() {
return id;
}
public String getName() {
return name;
}
public Optional<String> getEmailAddress() {
return Optional.ofNullable(emailAddress);
}
public boolean isVerified() {
return isVerified;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public List<Integer> getFriendUserIds() {
return friendUserIds;
}
public static Builder builder(int id, String name) {
return new Builder(id, name);
}
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 withEmailAddress(String emailAddress){
this.emailAddress = emailAddress;
return this;
}
public Builder withVerified(boolean isVerified){
this.emailAddress = emailAddress;
return this;
}
public Builder withCreatedAt(LocalDateTime createdAt){
this.emailAddress = emailAddress;
return this;
}
public Builder withFriendUserIds(List<Integer> friendUserIds){
this.friendUserIds = friendUserIds;
return this;
}
public User build() {
return new User(this);
}
}
}
- User 의 내부 static 클래스로 Builder 클래스를 만들고 User 의 static 메소드로 builder 메소드를 구현한다.
- 마지막으로 Builder 클래스 내부에 User 를 반환하는 build() 메소드를 구현함으로써 User 와 Builder 를 연결해준다.
- 이렇게 되면 아래와 같이 빌더 패턴을 이용해서 객체를 생성 할 수 있다.
User user = User.builder(1, "hyunwook")
.withEmailAddress("hyunwook@gmail.com")
.withVerified(true)
.build();
빌더 패턴에 함수형 프로그래밍 적용
public Builder withEmailAddress(String emailAddress){
this.emailAddress = emailAddress;
return this;
}
public Builder withVerified(boolean isVerified){
this.emailAddress = emailAddress;
return this;
}
public Builder withCreatedAt(LocalDateTime createdAt){
this.emailAddress = emailAddress;
return this;
}
public Builder withFriendUserIds(List<Integer> friendUserIds){
this.friendUserIds = friendUserIds;
return this;
}
- User 클래스 내부에 위와 같은 메소드를 아래와 같이 바꿀 수 있다.
public Builder with(Consumer<Builder> consumer) {
consumer.accept(this);
return this;
}
- 기존의 4개의 메소드(setter)를 1개의 메소드로 줄일 수 있다. -> 기존 방식보다 훨씬 간단하게 빌더 패턴을 구현할 수 있다. (메소드가 많아질 수록 더욱 효과적이다.)
- 그렇게 되면 아래와 같이 빌더 패턴을 적용할 수 있다.
User user = User.builder(1, "Alice")
.with(builder -> {
builder.emailAddress = "alice@fastcampus.co.kr";
builder.isVerified = true;
}).build();
Decorator Pattern
- 구조 패턴의 하나이다.
- 용도에 따라 객체에 기능을 계속 추가(decorate)할 수 있게 해준다.
- 먼저 Decorator Pattern 을 적용하기 위해서 아래와 같이 Functional Interface 를 하나 구현해 준다.
@FunctionalInterface
public interface PriceProcessor {
Price process(Price price);
default PriceProcessor andThen(PriceProcessor next) {
return price -> next.process(process(price));
}
}
- Functional Interface 이기 때문에 1개의 Abstract 메소드를 갖는다.
- andThen 메소드는 process(price) 에서 먼저 자신먼저 process 를 해주고 인자로 받은 다음 PriceProcessor 를 process 해준다.
- Functional Interface 이기 때문에 람다식을 적용할 수 있다.
- 다음으로는 PriceProcessor 를 구현한 여러 Processor 들을 아래와 같이 구현한다.
public class BasicPriceProcessor implements PriceProcessor {
@Override
public Price process(Price price) {
return price;
}
}
public class DiscountPriceProcessor implements PriceProcessor {
@Override
public Price process(Price price) {
return new Price(price.getPrice() + ", then applied discount");
}
}
public class TaxPriceProcessor implements PriceProcessor {
@Override
public Price process(Price price) {
return new Price(price.getPrice() + ", then applied tax");
}
}
- 위와 같이 3개의 Processor 를 구현했으니 아래와 같이 Decorator Fattern 을 적용할 수 있다.
Decorator Pattern 에 함수형 프로그래밍 적용
Price unprocessedPrice = new Price("Original Price");
PriceProcessor basicPriceProcessor = new BasicPriceProcessor();
PriceProcessor discountPriceProcessor = new DiscountPriceProcessor();
PriceProcessor taxPriceProcessor = new TaxPriceProcessor();
PriceProcessor decoratedPriceProcessor = basicPriceProcessor
.andThen(discountPriceProcessor)
.andThen(taxPriceProcessor);
// .andThen(price -> new Price(price.getPrice() + ", then applied tax"));
Price processedPrice = decoratedPriceProcessor.process(unprocessedPrice);
- 위와 같이 추가적인 기능을 끼워넣어서 새로운 기능을 만들 수 있다.
- 만약 클래스를 만들기 싫다면 주석처리 한것처럼 람다식을 이용해서 처리할 수 있다. -> 하지만 재사용할 필요가 있다면 클래스로 구현하는것이 좋다.
Strategy Pattern
- 대표적인 행동 패턴이다.
- 런타임에 어떤 전략(알고리즘)을 사용할 지 선택할 수 있게 해준다.
- 전략들을 캡슐화하여 간단하게 교체할 수 있게 해준다.
- 먼저 Strategy Pattern 을 적용하기 위해서 아래와 같이 Interface 를 하나 구현해 준다.
public interface EmailProvider {
String getEmail(User user);
}
- 위의 interface 는 Abstract 메소드 1개만을 가지고 있기 때문에 Functional Interface 이다.
- 다음으로는 위의 인터페이스를 구현한 2개의 Provider 를 구현한다.
public class VerifyYourEmailAddressEmailProvider implements EmailProvider {
@Override
public String getEmail(User user) {
return "'Verify Your Email Address' email for " + user.getName();
}
}
public class MakeMoreFriendsEmailProvider implements EmailProvider {
@Override
public String getEmail(User user) {
return "'Make More Friends' email for " + user.getName();
}
}
- 이제 아래와 같이 Strategy Pattern 을 적용할 수 있다. (User 와 관련된 코드는 생략한다.)
Strategy Pattern 에 함수형 프로그래밍 적용
emailSender.setEmailProvider(verifyYourEmailAddressEmailProvider); // 전략 주입
users.stream()
.filter(user -> !user.isVerified())
.forEach(emailSender::sendEmail);
emailSender.setEmailProvider(makeMoreFriendsEmailProvider); // 전략 변경
users.stream()
.filter(User::isVerified)
.filter(user -> user.getFriendUserIds().size() <= 5)
.forEach(emailSender::sendEmail);
- 전략이 변경됨에 따라 sendEmail 메소드의 결과 값이 달라진다.
- 하지만 이렇게 전략을 만들때마다 클래스를 만들어 줘야할까? -> EmailProvider 가 Functional Interface 이기 때문에 그렇지 클래스를 따로 구현하지 않고 아래와 같이 람다식으로 대체할 수 있다.
emailSender.setEmailProvider(user -> "'Verify Your Email Address' email for " + user.getName());
users.stream()
.filter(User::isVerified)
.filter(user -> user.getFriendUserIds().size() > 5)
.forEach(emailSender::sendEmail);
emailSender.setEmailProvider(user -> "'Make More Friends' email for " + user.getName());
users.stream()
.filter(User::isVerified)
.filter(user -> user.getFriendUserIds().size() > 5)
.forEach(emailSender::sendEmail);
- 이렇게 되면 굳이 VerifyYourEmailAddressEmailProvider 클래스와 MakeMoreFriendsEmailProvider 클래스를 구현하지 않고도 Strategy Pattern 을 적용시킬 수 있다.
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");
}
}
}
- createUser 메소드를 통해서 뼈대만 만들어두고 validateUser, writeToDB 메소드의 디테일한 구현은 하위 클래스에게 맡긴다.
- 다음으로는 하위 클래스를 아래와 같이 구현한다.
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 InternalUserService extends AbstractUserService {
@Override
protected boolean validateUser(User user) {
System.out.println("validating internal user " + user.getName());
return true;
}
@Override
protected void writeToDB(User user) {
System.out.println("Writing user " + user.getName() + " to internal DB");
}
}
- 이제 아래와 같이 Template Method Pattern 을 적용시킬 수 있다.
Template Method Pattern 에 함수형 프로그래밍 적용
UserService userService = new UserService();
InternalUserService internalUserService = new InternalUserService();
userService.createUser(alice);
internalUserService.createUser(alice);
- 위에 코드에서 userService, internalUserService 둘다 createUser 메소드를 실행시켰지만 내부 구현 로직이 다르기 때문에 다른 결과를 반환한다.
- 하지만 하위 클래스가 많이지면 하나하나 클래스로 구현해주기는 번거롭다 -> 아래와 같이 함수형 인터페이스를 적용시켜서 해결할 수 있다.
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");
}
}
}
- 먼저 추상 메소드로 구현했던 클래스를 위와 같이 함수형 인터페이스를 인자로 가지는 클래스로 대체한다.
- 그렇게 되면 아래와 같이 따로 클래스를 구현하지 않고 Template Method Pattern 을 적용시킬 수 있다.
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);
- Predicate, Consumer 타입을 인자를 람다식으로 전달해서 UserServiceInFunctionalWay 객체를 만들었다.
- 이렇게 하면 따로 하위 클래스를 구현하지 않아도 된다.
Chain of Responsibility Pattern
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));
}
}
- 생성자에서 Consumer 타입의 processOrder 을 세팅해 준다.
- setNext 메소드를 보면, 재귀 방식으로 맨 뒤까지 이동한뒤에 set 해준다. 이렇게 되면 뒤에 계속해서 OrderProcessStep 을 붙일 수 있다.
- process 메소드는 Order 를 받아서 processOrder 를 실행시켜준다. 또한 다음 step 이 있다면 다음 step 도 실행시킨다.
public class Chapter10Section6 {
public static void main(String[] args) {
OrderProcessStep initializeStep = new OrderProcessStep(order -> {
if (order.getStatus() == OrderStatus.CREATED) {
System.out.println("Start processing order " + order.getId());
order.setStatus(OrderStatus.IN_PROGRESS);
}
});
OrderProcessStep setOrderAmountStep = new OrderProcessStep(order -> {
if (order.getStatus() == OrderStatus.IN_PROGRESS) {
System.out.println("Setting amount of order " + order.getId());
order.setAmount(order.getOrderLines().stream()
.map(OrderLine::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add));
}
});
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);
}
}
});
OrderProcessStep processPaymentStep = new OrderProcessStep(order -> {
if (order.getStatus() == OrderStatus.IN_PROGRESS) {
System.out.println("Processing payment of order " + order.getId());
order.setStatus(OrderStatus.PROCESSED);
}
});
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 completeProcessingOrderStep = new OrderProcessStep(order -> {
if(order.getStatus() == OrderStatus.PROCESSED) {
System.out.println("Finished processing order " + order.getId());
}
});
OrderProcessStep chainedOrderProcessSteps = initializeStep
.setNext(setOrderAmountStep)
.setNext(verifyOrderStep)
.setNext(processPaymentStep)
.setNext(handleErrorStep)
.setNext(completeProcessingOrderStep);
Order order = new Order()
.setId(1001L)
.setStatus(OrderStatus.CREATED)
.setOrderLines(Arrays.asList(
new OrderLine().setAmount(BigDecimal.valueOf(1000)),
new OrderLine().setAmount(BigDecimal.valueOf(2000))));
chainedOrderProcessSteps.process(order);
Order failingOrder = new Order()
.setId(1002L)
.setStatus(OrderStatus.CREATED)
.setOrderLines(Arrays.asList(
new OrderLine().setAmount(BigDecimal.valueOf(1000)),
new OrderLine().setAmount(BigDecimal.valueOf(-2000))));
chainedOrderProcessSteps.process(failingOrder);
}
}
- 먼저 OrderProcessStep 타입의 step 들을 만들어 준다.
- 그 다음 setNext 메소드를 통해서 step 을 연결한 chainedOrderProcessSteps 를 만든다.
- 그 다음 Order 객체를 만들어주고, chainedOrderProcessSteps 에 process 를 하게 되면 연결 된 step 들을 순차적으로 검사한다.
REFERENCES