Skip to the content.
package com.bookshop.creational.builder;

import com.bookshop.domain.Book;
import com.bookshop.domain.Customer;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

/**
 * A customer order with two required fields and several optional ones -- the classic
 * situation where telescoping constructors become unreadable and a builder shines.
 */
public final class Order {

    private final Customer customer;
    private final List<Book> books;
    private final String giftMessage;
    private final String deliveryInstructions;
    private final String promoCode;
    private final boolean expressDelivery;

    private Order(Builder builder) {
        this.customer = builder.customer;
        this.books = List.copyOf(builder.books);
        this.giftMessage = builder.giftMessage;
        this.deliveryInstructions = builder.deliveryInstructions;
        this.promoCode = builder.promoCode;
        this.expressDelivery = builder.expressDelivery;
    }

    /** Entry point: {@code Order.forCustomer(alice).add(book).express().build()}. */
    public static Builder forCustomer(Customer customer) {
        return new Builder(customer);
    }

    public Customer customer() {
        return customer;
    }

    public List<Book> books() {
        return books;
    }

    public Optional<String> giftMessage() {
        return Optional.ofNullable(giftMessage);
    }

    public Optional<String> deliveryInstructions() {
        return Optional.ofNullable(deliveryInstructions);
    }

    public Optional<String> promoCode() {
        return Optional.ofNullable(promoCode);
    }

    public boolean expressDelivery() {
        return expressDelivery;
    }

    public BigDecimal subtotal() {
        return books.stream().map(Book::price).reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    public static final class Builder {

        private final Customer customer;
        private final List<Book> books = new ArrayList<>();
        private String giftMessage;
        private String deliveryInstructions;
        private String promoCode;
        private boolean expressDelivery;

        private Builder(Customer customer) {
            this.customer = Objects.requireNonNull(customer, "customer");
        }

        public Builder add(Book book) {
            books.add(Objects.requireNonNull(book, "book"));
            return this;
        }

        public Builder giftMessage(String message) {
            this.giftMessage = message;
            return this;
        }

        public Builder deliveryInstructions(String instructions) {
            this.deliveryInstructions = instructions;
            return this;
        }

        public Builder promoCode(String code) {
            this.promoCode = code;
            return this;
        }

        public Builder express() {
            this.expressDelivery = true;
            return this;
        }

        /** Validation lives in one place, and the resulting {@link Order} is immutable. */
        public Order build() {
            if (books.isEmpty()) {
                throw new IllegalStateException("an order must contain at least one book");
            }
            return new Order(this);
        }
    }
}

Raw file