Based on the requirements and use cases, identify the main objects of the system and analyze how they interact and relate to each other...
For the vending machine scenario, I've attached a mermaid class diagram illustrating the important objects and relations:
At a high level, this problem involves 2 major patterns: State pattern and Strategy pattern. State pattern is used for the different states the vending machine could be in, ie: idle, product selected, out of order state, etc. The Strategy pattern is used based on the type of payment the user uses at time of purchase, ie: Coins, Cash, Card. Everything else is DoA's with SOLID principles applied in terms of separation of concerns. The VendingMachine Class orchestrates all the operations between all the different services and behavioral patterns.
For each class, define the attributes (data) it will hold and the methods (functions) that operate on the attributes. Ensure they align with the object's responsibilities and adhere to the principle of encapsulation. Write your code in the code editor below.
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
enum PaymentType {
COIN, CASH, CARD
}
enum OrderStatus {
CREATED, PAID, DISPENSED, REFUNDED, FAILED
}
class Product {
private final String code;
private final String name;
private final long priceInCents;
public Product(String code, String name, long priceInCents) {
this.code = code;
this.name = name;
this.priceInCents = priceInCents;
}
public String getCode() {
return code;
}
public String getName() {
return name;
}
public long getPriceInCents() {
return priceInCents;
}
}
class InventoryItem {
private final Product product;
private int quantity;
public InventoryItem(Product product, int quantity) {
this.product = product;
this.quantity = quantity;
}
public Product getProduct() {
return product;
}
public int getQuantity() {
return quantity;
}
public void addQuantity(int amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Quantity must be greater than 0.");
}
quantity += amount;
}
public void decrementQuantity() {
if (quantity <= 0) {
throw new IllegalStateException("Product is out of stock.");
}
quantity--;
}
}
class Payment {
private final PaymentType type;
private final long amountInCents;
public Payment(PaymentType type, long amountInCents) {
this.type = type;
this.amountInCents = amountInCents;
}
public PaymentType getType() {
return type;
}
public long getAmountInCents() {
return amountInCents;
}
}
class Order {
private final int orderId;
private Product product;
private long amountPaidInCents;
private long changeInCents;
private OrderStatus status;
public Order(int orderId, Product product) {
this.orderId = orderId;
this.product = product;
this.status = OrderStatus.CREATED;
}
public int getOrderId() {
return orderId;
}
public Product getProduct() {
return product;
}
public long getAmountPaidInCents() {
return amountPaidInCents;
}
public void addPayment(long amountInCents) {
this.amountPaidInCents += amountInCents;
}
public long getChangeInCents() {
return changeInCents;
}
public void setChangeInCents(long changeInCents) {
this.changeInCents = changeInCents;
}
public OrderStatus getStatus() {
return status;
}
public void setStatus(OrderStatus status) {
this.status = status;
}
}
class Receipt {
private final int orderId;
private final String productCode;
private final long priceInCents;
private final long amountPaidInCents;
private final long changeInCents;
public Receipt(int orderId, String productCode, long priceInCents,
long amountPaidInCents, long changeInCents) {
this.orderId = orderId;
this.productCode = productCode;
this.priceInCents = priceInCents;
this.amountPaidInCents = amountPaidInCents;
this.changeInCents = changeInCents;
}
@Override
public String toString() {
return "Receipt{" +
"orderId=" + orderId +
", productCode='" + productCode + '\'' +
", price=$" + priceInCents / 100.0 +
", paid=$" + amountPaidInCents / 100.0 +
", change=$" + changeInCents / 100.0 +
'}';
}
}
class InventoryService {
private final Map<String, InventoryItem> inventory = new ConcurrentHashMap<>();
public boolean isInStock(String code) {
return inventory.containsKey(code) && inventory.get(code).getQuantity() > 0;
}
public Product getProduct(String code) {
InventoryItem item = inventory.get(code);
if (item == null) {
throw new IllegalArgumentException("Product code not found: " + code);
}
return item.getProduct();
}
public synchronized void addProduct(Product product, int quantity) {
if (inventory.containsKey(product.getCode())) {
inventory.get(product.getCode()).addQuantity(quantity);
} else {
inventory.put(product.getCode(), new InventoryItem(product, quantity));
}
}
public synchronized void decrementStock(String code) {
if (!isInStock(code)) {
throw new IllegalStateException("Product is out of stock.");
}
inventory.get(code).decrementQuantity();
}
public synchronized void restock(String code, int quantity) {
if (!inventory.containsKey(code)) {
throw new IllegalArgumentException("Product code not found: " + code);
}
inventory.get(code).addQuantity(quantity);
}
public int getQuantity(String code) {
if (!inventory.containsKey(code)) {
return 0;
}
return inventory.get(code).getQuantity();
}
}
interface PaymentStrategy {
boolean pay(Order order, Payment payment);
void refund(Order order);
}
class CoinPaymentStrategy implements PaymentStrategy {
@Override
public boolean pay(Order order, Payment payment) {
order.addPayment(payment.getAmountInCents());
return order.getAmountPaidInCents() >= order.getProduct().getPriceInCents();
}
@Override
public void refund(Order order) {
System.out.println("Refunding coins: $" + order.getAmountPaidInCents() / 100.0);
}
}
class CashPaymentStrategy implements PaymentStrategy {
@Override
public boolean pay(Order order, Payment payment) {
order.addPayment(payment.getAmountInCents());
return order.getAmountPaidInCents() >= order.getProduct().getPriceInCents();
}
@Override
public void refund(Order order) {
System.out.println("Refunding cash: $" + order.getAmountPaidInCents() / 100.0);
}
}
class CardPaymentStrategy implements PaymentStrategy {
@Override
public boolean pay(Order order, Payment payment) {
order.addPayment(payment.getAmountInCents());
return order.getAmountPaidInCents() >= order.getProduct().getPriceInCents();
}
@Override
public void refund(Order order) {
System.out.println("Refunding card payment: $" + order.getAmountPaidInCents() / 100.0);
}
}
class PaymentService {
public boolean processPayment(Order order, Payment payment) {
PaymentStrategy strategy = getStrategy(payment.getType());
boolean paid = strategy.pay(order, payment);
if (paid) {
long change = order.getAmountPaidInCents() - order.getProduct().getPriceInCents();
order.setChangeInCents(change);
order.setStatus(OrderStatus.PAID);
}
return paid;
}
public void refund(Order order, PaymentType paymentType) {
PaymentStrategy strategy = getStrategy(paymentType);
strategy.refund(order);
order.setStatus(OrderStatus.REFUNDED);
}
private PaymentStrategy getStrategy(PaymentType type) {
return switch (type) {
case COIN -> new CoinPaymentStrategy();
case CASH -> new CashPaymentStrategy();
case CARD -> new CardPaymentStrategy();
};
}
}
class DispenseResult {
private final boolean success;
private final boolean jammed;
public DispenseResult(boolean success, boolean jammed) {
this.success = success;
this.jammed = jammed;
}
public boolean isSuccess() {
return success;
}
public boolean isJammed() {
return jammed;
}
}
class DispenseService {
private boolean simulateJam = false;
public void setSimulateJam(boolean simulateJam) {
this.simulateJam = simulateJam;
}
public DispenseResult dispenseProduct(Product product) {
if (simulateJam) {
return new DispenseResult(false, true);
}
System.out.println("Dispensing product: " + product.getName());
return new DispenseResult(true, false);
}
}
interface State {
void selectProduct(VendingMachine context, String code);
void insertPayment(VendingMachine context, Payment payment);
void dispense(VendingMachine context);
void refund(VendingMachine context);
}
class IdleState implements State {
@Override
public void selectProduct(VendingMachine context, String code) {
InventoryService inventoryService = context.getInventoryService();
if (!inventoryService.isInStock(code)) {
System.out.println("Product is out of stock or invalid.");
return;
}
Product product = inventoryService.getProduct(code);
Order order = context.createOrder(product);
context.setCurrentOrder(order);
context.setState(new ProductSelectedState());
System.out.println("Selected product: " + product.getName()
+ " | Price: $" + product.getPriceInCents() / 100.0);
}
@Override
public void insertPayment(VendingMachine context, Payment payment) {
System.out.println("Select a product first.");
}
@Override
public void dispense(VendingMachine context) {
System.out.println("Select and pay for a product first.");
}
@Override
public void refund(VendingMachine context) {
System.out.println("No active order to refund.");
}
}
class ProductSelectedState implements State {
@Override
public void selectProduct(VendingMachine context, String code) {
System.out.println("Product already selected. Complete or cancel current order.");
}
@Override
public void insertPayment(VendingMachine context, Payment payment) {
Order order = context.getCurrentOrder();
boolean paid = context.getPaymentService().processPayment(order, payment);
if (paid) {
System.out.println("Payment accepted.");
context.setLastPaymentType(payment.getType());
context.setState(new HasPaymentState());
} else {
System.out.println("Partial payment accepted. Remaining: $"
+ (order.getProduct().getPriceInCents() - order.getAmountPaidInCents()) / 100.0);
}
}
@Override
public void dispense(VendingMachine context) {
System.out.println("Payment required before dispensing.");
}
@Override
public void refund(VendingMachine context) {
Order order = context.getCurrentOrder();
if (order != null && context.getLastPaymentType() != null) {
context.getPaymentService().refund(order, context.getLastPaymentType());
}
context.clearCurrentOrder();
context.setState(new IdleState());
}
}
class HasPaymentState implements State {
@Override
public void selectProduct(VendingMachine context, String code) {
System.out.println("Already paid. Dispense current product first.");
}
@Override
public void insertPayment(VendingMachine context, Payment payment) {
System.out.println("Payment already completed.");
}
@Override
public void dispense(VendingMachine context) {
context.setState(new DispensingState());
context.dispense();
}
@Override
public void refund(VendingMachine context) {
Order order = context.getCurrentOrder();
context.getPaymentService().refund(order, context.getLastPaymentType());
context.clearCurrentOrder();
context.setState(new IdleState());
}
}
class DispensingState implements State {
@Override
public void selectProduct(VendingMachine context, String code) {
System.out.println("Currently dispensing. Please wait.");
}
@Override
public void insertPayment(VendingMachine context, Payment payment) {
System.out.println("Currently dispensing. Please wait.");
}
@Override
public void dispense(VendingMachine context) {
Order order = context.getCurrentOrder();
Product product = order.getProduct();
DispenseResult result = context.getDispenseService().dispenseProduct(product);
if (result.isSuccess()) {
context.getInventoryService().decrementStock(product.getCode());
order.setStatus(OrderStatus.DISPENSED);
Receipt receipt = new Receipt(
order.getOrderId(),
product.getCode(),
product.getPriceInCents(),
order.getAmountPaidInCents(),
order.getChangeInCents()
);
System.out.println(receipt);
context.clearCurrentOrder();
context.setState(new IdleState());
} else if (result.isJammed()) {
System.out.println("Mechanical jam detected. Issuing refund.");
context.getPaymentService().refund(order, context.getLastPaymentType());
context.setState(new OutOfOrderState());
}
}
@Override
public void refund(VendingMachine context) {
System.out.println("Cannot manually refund while dispensing.");
}
}
class OutOfOrderState implements State {
@Override
public void selectProduct(VendingMachine context, String code) {
System.out.println("Machine is out of order.");
}
@Override
public void insertPayment(VendingMachine context, Payment payment) {
System.out.println("Machine is out of order.");
}
@Override
public void dispense(VendingMachine context) {
System.out.println("Machine is out of order.");
}
@Override
public void refund(VendingMachine context) {
System.out.println("Machine is out of order.");
}
}
class VendingMachine {
private State currentState;
private final InventoryService inventoryService;
private final PaymentService paymentService;
private final DispenseService dispenseService;
private Order currentOrder;
private PaymentType lastPaymentType;
private final AtomicInteger orderIdGenerator = new AtomicInteger(1);
public VendingMachine() {
this.inventoryService = new InventoryService();
this.paymentService = new PaymentService();
this.dispenseService = new DispenseService();
this.currentState = new IdleState();
}
public void selectProduct(String code) {
currentState.selectProduct(this, code);
}
public void insertPayment(Payment payment) {
currentState.insertPayment(this, payment);
}
public void dispense() {
currentState.dispense(this);
}
public void refund() {
currentState.refund(this);
}
public void restock(Product product, int quantity) {
inventoryService.addProduct(product, quantity);
}
public Order createOrder(Product product) {
return new Order(orderIdGenerator.getAndIncrement(), product);
}
public void setState(State state) {
this.currentState = state;
}
public InventoryService getInventoryService() {
return inventoryService;
}
public PaymentService getPaymentService() {
return paymentService;
}
public DispenseService getDispenseService() {
return dispenseService;
}
public Order getCurrentOrder() {
return currentOrder;
}
public void setCurrentOrder(Order currentOrder) {
this.currentOrder = currentOrder;
}
public void clearCurrentOrder() {
this.currentOrder = null;
this.lastPaymentType = null;
}
public PaymentType getLastPaymentType() {
return lastPaymentType;
}
public void setLastPaymentType(PaymentType lastPaymentType) {
this.lastPaymentType = lastPaymentType;
}
}
Trade-offs:
SOLID: