Skip to content

Instantly share code, notes, and snippets.

@naneun
Last active February 4, 2022 07:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save naneun/0207d6b17e3fd2fd3f73af06e1c3439b to your computer and use it in GitHub Desktop.
Save naneun/0207d6b17e3fd2fd3f73af06e1c3439b to your computer and use it in GitHub Desktop.
CS08
  • 주문 이벤트Ⓜ️ + 동시 주문과 바리스타Ⓜ️

    • 디렉토리 구조

      SmartSelectImage_2022-02-02-21-55-08

    • 이벤트 흐름도

      SmartSelectImage_2022-02-03-03-42-52

    • 시나리오

      • First input
        • A, 1:3, 2:3, 3:1
        • B, 2:3, 3:2
        • C, 3:5
      • Second input
        • D, 1:5
        • E, 2:1, 3:1
        • F, 1:1, 2:2
      • Third input
        • G, 3:3
        • H, 3:1
      • Fourth input
        • I, 2:2
    • 기능요구사항

      • 주문 담당자(Cashier)는 음료 주문을 연속해서 받을 수 있다.
      • 음료 주문을 받으면 주문 대기표(Queue)에 추가한다.
        • ✔ Drink < Order < OrderInfo -> Event
        • ✔ EventQueue 를 통해 Event 를 전달한다
      • 주문 대기표도 이벤트를 받아서 처리하는 별도 모듈/객체로 분리해서 구현한다.
        • ✔ OrderSheet
      • 매니저(Manager)는 음료를 확인하기 위해서 주문 대기표를 1초마다 확인한다.
      • 주문이 있을 경우 작업이 비어있는 (제작할 수 있는) 바리스타에게 작업 이벤트를 전달한다.
        • ❗ 주문을 먼저 특정 바리스타에게 할당, 해당 바리스타가 여유가 있을 때 제조 시작
      • 바리스타가 보낸 특정 고객의 음료 제작 완료 이벤트를 받으면 결과를 출력한다.
        • ✔ Publisher, Subscriber 패턴으로 구현
      • 바리스타(Barista)는 동시에 2개까지 음료를 만들 수 있다고 가정한다.
      • 스레드를 직접 생성하는 게 아니라 이벤트 방식으로 동작해야 한다.
      • 바리스타는 음료를 만들기 시작할 때와 끝날 때 마다 이벤트를 발생한다.
      • 이벤트가 발생할 때마다 음료 작업에 대한 로그를 출력한다.
      • 프로그램을 시작할 때 바리스타 인원수를 결정할 수 있도록 구현한다.
      • 프로그램이 시작하면 바리스타가 몇 명인지 출력한다.
      • 주문 담당자와 매니저는 각각 1명이라고 가정한다.
      • 주문 담당자는 음료 주문을 받을 때마다 한꺼번에 주문한 고객을 구분해야 한다.
      • 매니저는 바리스타가 보낸 음료 작업 완료 이벤트를 받아서 고객별로 주문 완성표를 업데이트한다.
        • ✔ OrderSheet 클래스의 markOrder 메서드
      • 현황판에는 주문한 고객별로 주문한 음료를 모두 구분해서 표시해야 한다.
      • 주문부터 제작 완료까지 이벤트 흐름을 그림으로 그려서, gist에 함께 첨부한다.
      • 앞 단계에 이어서 각 언어와 플랫폼에서 제공하는 비동기 이벤트 처리 방식을 활용한다
      • 주문 담당자 / 주문대기큐 / 매니저 / 바리스타 / 현황판과 주문 음료, 완성 음료에 대한 모듈 또는 파일을 구분해서 작업한다
      • 고객이 주문한 모든 음료가 완성되면 각자 자기만의 방식으로 특별하게(?) 표시해서 출력한다.
      • 모든 음료수를 만들고 나서 3초 동안 주문이 없으면 프로그램을 종료한다. (😢 미흡)
        • ❗ 특정 시간 동안 주문이 없으면 Timeout 을 발생시키는 로직과 현재 제조하고 있는 음료가 없음을 나타내는 변수의 동작이 분리되어있다.
    • 추가 요구 사항

      • 바리스타가 3명 이상일 경우 특정 바리스타가 담당하는 음료수를 분배하는 방식을 구현한다. 모든 바리스타가 모든 음료수를 만들지 않아야 한다.
        • ✔ BaristaGroup 으로 바리스타들을 관리한다
        • ✔ 소속 바리스타 중 당장 음료를 제조할 수 있는 타겟을 설정하여 주문을 알린다
      • 바리스타 4명, 주문 고객 9명 + 음료 30개를 주문했다고 가정하고 동작을 확인한다.
    • 실행결과

      Week1-1.C__Users_rmtlr_CodeMasters_Week4_Cafe_README.md.2022-02-03.04-23-38.mp4
package com.my.cs08.cafe.staff;
import com.my.cs08.order.Drink;
import com.my.cs08.util.OutputView;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class Barista implements Comparable<Barista> {
private static final int MAX_WORKLOAD = 2;
private int id;
private OutputView outputView;
private AtomicInteger workload;
private ExecutorService executor;
public Barista(int id) {
this.id = id;
this.outputView = OutputView.getInstance();
this.workload = new AtomicInteger();
this.executor = Executors.newFixedThreadPool(MAX_WORKLOAD); /* Maximum 2 Tasks */
}
public synchronized boolean afford() {
return workload.get() < MAX_WORKLOAD;
}
public void make(Drink drink) {
CompletableFuture.runAsync(() -> doing(drink), executor)
.exceptionally(ex -> {
outputView.errMsg(ex);
return null;
})
.join();
}
private void doing(Drink drink) {
try {
outputView.doingMsg(id, drink);
workload.set(workload.get() + 1);
TimeUnit.SECONDS.sleep(drink.getTime());
outputView.doneMsg(id, drink);
workload.set(workload.get() - 1);
} catch (InterruptedException e) {
throw new CompletionException(e);
}
}
@Override
public int compareTo(Barista o) {
return Integer.compare(this.workload.get(), o.workload.get());
}
}
package com.my.cs08.cafe.staff;
import com.my.cs08.cafe.role.Publisher;
import com.my.cs08.cafe.role.Subscriber;
import com.my.cs08.event.Event;
import com.my.cs08.util.OutputView;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BaristaGroup implements Publisher<Event>, Subscriber<Event> {
private OutputView outputView;
private List<Subscriber> subscribers;
private List<Barista> baristas;
private ExecutorService executor;
public BaristaGroup(int count) {
this.outputView = OutputView.getInstance();
this.subscribers = new ArrayList<>();
this.baristas = new ArrayList<>();
for (int idx = 0; idx < count; ++idx) {
this.baristas.add(new Barista(idx + 1));
}
this.executor = Executors.newFixedThreadPool(count);
}
@Override
public void subscribe(Subscriber<? super Event> subscriber) {
subscribers.add(subscriber);
}
public void notifyAllSubscribers(Event event) {
subscribers.forEach(subscriber -> {
subscriber.onNext(event);
});
}
@Override
public void onNext(Event event) {
makeDrinkAsync(event);
}
private void makeDrinkAsync(Event event) {
CompletableFuture.supplyAsync(() -> {
Barista barista = null;
while (!(barista = assign()).afford());
barista.make(event.getOrder().getDrink());
return event;
}, executor)
.thenAccept((result) -> {
notifyAllSubscribers(result);
})
.exceptionally(ex -> {
outputView.errMsg(ex);
return null;
});
}
private Barista assign() {
return baristas.stream().sorted().findFirst().get();
}
}
package com.my.cs08.cafe;
import com.my.cs08.cafe.staff.BaristaGroup;
import com.my.cs08.cafe.staff.Cashier;
import com.my.cs08.cafe.staff.Manager;
import com.my.cs08.event.EventQueue;
import com.my.cs08.order.OrderSheet;
import com.my.cs08.util.InputView;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
public class Cafe {
public static AtomicBoolean isOpen;
public static AtomicBoolean inProgress;
private InputView inputView;
private Cashier cashier;
private Manager manager;
private BaristaGroup baristaGroup;
private OrderSheet orderSheet;
private EventQueue eventQueue;
public Cafe() throws IOException {
this.isOpen = new AtomicBoolean(false);
this.inputView = InputView.getInstance();
this.inProgress = new AtomicBoolean(false);
this.orderSheet = new OrderSheet();
this.eventQueue = new EventQueue();
this.cashier = new Cashier(orderSheet, eventQueue);
this.manager = new Manager(orderSheet, eventQueue);
this.baristaGroup = new BaristaGroup(inputView.getBaristaCount());
}
public void open() {
isOpen.set(true);
cashier.takeOrderAsync();
manager.subscribe(baristaGroup);
manager.checkOrderAsync();
baristaGroup.subscribe(manager);
}
}
package com.my.cs08.cafe.staff;
import com.my.cs08.cafe.Cafe;
import com.my.cs08.event.EventQueue;
import com.my.cs08.order.OrderInfo;
import com.my.cs08.order.OrderSheet;
import com.my.cs08.util.InputView;
import com.my.cs08.util.OutputView;
import java.util.concurrent.*;
public class Cashier {
private static final int TIMEOUT = 10;
private InputView inputView;
private OutputView outputView;
private OrderSheet orderSheet;
private EventQueue eventQueue;
private ExecutorService executor;
public Cashier(OrderSheet orderSheet, EventQueue eventQueue) {
this.inputView = InputView.getInstance();
this.outputView = OutputView.getInstance();
this.orderSheet = orderSheet;
this.eventQueue = eventQueue;
this.executor = Executors.newFixedThreadPool(1);
}
public void takeOrderAsync() {
CompletableFuture.runAsync(this::takeOrder);
}
private void takeOrder() {
while (Cafe.isOpen.get()) {
CompletableFuture.supplyAsync(this::takeInput, executor)
.orTimeout(TIMEOUT, TimeUnit.SECONDS)
.thenAccept(this::sendOrderInfo)
.exceptionally(this::exceptionCheck)
.join();
}
executor.shutdown();
outputView.closeMsg();
}
private OrderInfo takeInput() {
try {
inputView.commandInfo();
OrderInfo orderInfo = inputView.parsing(inputView.commandLine());
Cafe.inProgress.set(true);
return orderInfo;
} catch (Exception ex) {
throw new CompletionException(ex);
}
}
private void sendOrderInfo(OrderInfo orderInfo) {
orderSheet.addOrder(orderInfo);
orderInfo.convertToEvents().forEach(event -> eventQueue.add(event));
};
private Void exceptionCheck(Throwable ex) {
Throwable tb = ex.getCause();
if (tb instanceof TimeoutException && !Cafe.inProgress.get()) {
Cafe.isOpen.set(false);
}
if (!(tb instanceof TimeoutException)) {
outputView.errMsg(tb);
}
return null;
};
}
package com.my.cs08.order;
public class Drink {
private int id;
private String name;
private int time;
public Drink(int id, String name, int time) {
this.id = id;
this.name = name;
this.time = time;
}
public static Drink getInstance(int id) {
return id == 1 ? new Drink(1, "Americano", 3)
: id == 2 ? new Drink(2, "CafeLatte", 5)
: new Drink(3, "Frappuccino", 10);
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public int getTime() {
return time;
}
@Override
public String toString() {
return "Drink{" +
"id=" + id +
", name='" + name + '\'' +
", time=" + time +
'}';
}
}
package com.my.cs08.event;
import com.my.cs08.order.Order;
public class Event {
private String customer;
private Order order;
public Event(String customer, Order order) {
this.customer = customer;
this.order = order;
}
public String getCustomer() {
return customer;
}
public Order getOrder() {
return order;
}
@Override
public String toString() {
return customer + order.getDrink().getId();
}
}
package com.my.cs08.event;
import java.util.concurrent.LinkedBlockingQueue;
public class EventQueue {
private LinkedBlockingQueue<Event> queue;
public EventQueue() {
this.queue = new LinkedBlockingQueue<>();
}
public Event poll() {
return queue.poll();
}
public Event take() throws InterruptedException {
return queue.take();
}
public void add(Event event) {
queue.add(event);
}
public void showElements() {
if (isEmpty()) {
return;
}
System.out.println(queue);
}
public boolean isEmpty() {
return queue.isEmpty();
}
}
package com.my.cs08.util;
import com.my.cs08.order.OrderInfo;
import java.io.*;
public class InputView {
private static InputView inputView;
private Parser parser;
private BufferedReader br;
private InputView() {
this.parser = Parser.getInstance();
this.br = new BufferedReader(new InputStreamReader(System.in));
}
public static InputView getInstance() {
if (inputView == null) {
inputView = new InputView();
}
return inputView;
}
public String commandLine() throws IOException {
return br.readLine();
}
public int getBaristaCount() throws IOException {
System.out.print("> 바리스타 총 인원 수: ");
int count = Integer.parseInt(commandLine());
System.out.println("> 바리스타는 총 " + count + "명입니다.");
return count;
}
public void commandInfo() {
System.out.println("> 메뉴 = 1. 아메리카노(3s) 2. 카페라떼(5s) 3. 프라프치노(10s)\n" +
"> 고객별로 주문할 음료 개수를 입력하세요. 예) A고객, 아메리카노 2개, 프라프치노 1개 => A, 1:2, 3:1");
}
public OrderInfo parsing(String input) {
return parser.parsing(input);
}
}
package com.my.cs08;
import com.my.cs08.cafe.Cafe;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
Cafe cafe = new Cafe();
cafe.open();
}
}
package com.my.cs08.cafe.staff;
import com.my.cs08.cafe.Cafe;
import com.my.cs08.cafe.role.Publisher;
import com.my.cs08.cafe.role.Subscriber;
import com.my.cs08.event.Event;
import com.my.cs08.event.EventQueue;
import com.my.cs08.order.OrderSheet;
import com.my.cs08.util.OutputView;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Manager implements Publisher<Event>, Subscriber<Event> {
private static final int CORE_POOL_SIZE = 1;
private static final int INITIAL_DELAY = 0;
private static final int PERIOD = 1;
private OutputView outputView;
private List<Subscriber> subscribers;
private OrderSheet orderSheet;
private EventQueue eventQueue;
private ScheduledExecutorService scheduledExecutor;
public Manager(OrderSheet orderSheet, EventQueue eventQueue) {
this.outputView = OutputView.getInstance();
this.subscribers = new ArrayList<>();
this.orderSheet = orderSheet;
this.eventQueue = eventQueue;
this.scheduledExecutor = Executors.newScheduledThreadPool(CORE_POOL_SIZE);
}
@Override
public void subscribe(Subscriber<? super Event> subscriber) {
subscribers.add(subscriber);
}
@Override
public void onNext(Event event) {
String customer = event.getCustomer();
orderSheet.markOrder(event);
if (orderSheet.readyOrder(customer)) {
outputView.readyMsg(customer, orderSheet.getOrderList(customer));
orderSheet.clearOrder(customer);
if (orderSheet.noMoreOrder()) {
Cafe.inProgress.set(false);
}
}
}
public void checkOrderAsync() {
scheduledExecutor.scheduleAtFixedRate(this::checkOrder
, INITIAL_DELAY, PERIOD, TimeUnit.SECONDS);
}
private void checkOrder() {
if (!eventQueue.isEmpty()) {
outputView.QueueState(eventQueue);
Event event = eventQueue.poll();
notifyBaristaGroup(event);
}
}
private void notifyBaristaGroup(Event event) {
subscribers.forEach((subscriber -> {
subscriber.onNext(event);
}));
}
}
package com.my.cs08.order;
public class Order {
private Drink drink;
private int count;
public Order(int id, int count) {
this.drink = Drink.getInstance(id);
this.count = count;
}
public Drink getDrink() {
return drink;
}
public int getCount() {
return count;
}
@Override
public String toString() {
return "Order{" +
"drink=" + drink +
", count=" + count +
'}';
}
}
package com.my.cs08.order;
import com.my.cs08.event.Event;
import java.util.ArrayList;
import java.util.List;
public class OrderInfo {
private String customer;
private List<Order> orders;
public OrderInfo(String customer, List<Order> orders) {
this.customer = customer;
this.orders = orders;
}
public String getCustomer() {
return customer;
}
public List<Order> getOrders() {
return orders;
}
public List<Event> convertToEvents() {
List<Event> events = new ArrayList<>();
orders.forEach(order -> {
int count = order.getCount();
while (count-- > 0) {
events.add(new Event(customer, order));
}
});
return events;
}
@Override
public String toString() {
return "OrderInfo{" +
"customer='" + customer + '\'' +
", orders=" + orders +
'}';
}
}
package com.my.cs08.order;
import com.my.cs08.cafe.staff.Manager;
import com.my.cs08.event.Event;
import java.util.*;
public class OrderSheet {
private Map<String, List<Order>> sheet;
private Map<String, Map<String, Integer>> orderMap;
public OrderSheet() {
this.sheet = new HashMap<>();
this.orderMap = new HashMap<>();
}
public List<Order> getOrderList(String customer) {
return sheet.get(customer);
}
public void addOrder(OrderInfo orderInfo) {
String customer = orderInfo.getCustomer();
List<Order> orders = orderInfo.getOrders();
sheet.put(customer, orders);
orderMap.put(customer, new HashMap<>());
Map<String, Integer> mp = orderMap.get(customer);
orders.forEach(order -> {
String str = customer + order.getDrink().getId();
if (!mp.containsKey(str)) {
mp.put(str, 0);
}
mp.put(str, mp.get(str) + order.getCount());
});
}
public void markOrder(Event event) {
String customer = event.getCustomer();
Map<String, Integer> mp = orderMap.get(customer);
String str = customer + event.getOrder().getDrink().getId();
mp.put(str, mp.get(str) - 1);
if (mp.get(str) == 0) {
mp.remove(str);
}
}
public boolean readyOrder(String customer) {
return orderMap.get(customer).size() == 0;
}
public void clearOrder(String customer) {
orderMap.remove(customer);
}
public boolean noMoreOrder() {
return orderMap.size() == 0;
}
}
package com.my.cs08.util;
import com.my.cs08.event.EventQueue;
import com.my.cs08.order.Drink;
import com.my.cs08.order.Order;
import com.my.cs08.order.OrderSheet;
import java.util.List;
public class OutputView extends OrderSheet {
private static OutputView outputView;
private OutputView() {}
public static OutputView getInstance() {
if (outputView == null) {
outputView = new OutputView();
}
return outputView;
}
public void QueueState(EventQueue eventQueue) {
eventQueue.showElements();
}
public void doingMsg(int id, Drink drink) {
System.out.println("Barista" + id + " - " + drink.getName() + " in the making");
}
public void doneMsg(int id, Drink drink) {
System.out.println("Barista" + id + " - " + drink.getName() + " completion");
}
public void errMsg(Throwable ex) {
System.out.println(ex);
}
public void readyMsg(String customer, List<Order> orderList) {
String info = orderList.stream().map(order ->
String.format("%s * %d, ", order.getDrink().getName(), order.getCount()))
.reduce(": ", (a, b) -> a + b);
System.out.println(customer + info.substring(0, info.length() - 2));
}
public void closeMsg() {
System.out.println("Cafe is closed and no longer accepting orders");
}
}
package com.my.cs08.util;
import com.my.cs08.order.Order;
import com.my.cs08.order.OrderInfo;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Parser {
private static Parser parser;
private Parser() {}
public static Parser getInstance() {
if (parser == null) {
parser = new Parser();
}
return parser;
}
private boolean isValid(String input) {
Pattern pattern = Pattern.compile("[A-Z], \\d+:\\d+(, \\d+:\\d+)*");
Matcher matcher = pattern.matcher(input);
return matcher.matches();
}
public OrderInfo parsing(String input) throws IllegalArgumentException {
if (!isValid(input)) {
throw new IllegalArgumentException("Invalid input");
}
Pattern pattern = Pattern.compile("[A-Z]");
Matcher matcher = pattern.matcher(input);
if (!matcher.find()) {
throw new IllegalArgumentException("Invalid input");
}
String customer = matcher.group();
pattern = Pattern.compile("\\d+:\\d+");
matcher = pattern.matcher(input);
int elements = 0;
while (matcher.find()) {
++elements;
}
pattern = Pattern.compile("\\d+");
matcher = pattern.matcher(input);
List<Order> orders = new ArrayList<>();
int id, count;
for (int i = 0; i < elements; ++i) {
if (!matcher.find()) {
throw new IllegalArgumentException("Invalid input");
}
id = Integer.parseInt(matcher.group());
if (!matcher.find()) {
throw new IllegalArgumentException("Invalid input");
}
count = Integer.parseInt(matcher.group());
orders.add(new Order(id, count));
}
return new OrderInfo(customer, orders);
}
}
package com.my.cs08.cafe.role;
public interface Publisher<T> {
void subscribe(Subscriber<? super T> subscriber);
}
package com.my.cs08.cafe.role;
public interface Subscriber<T> {
void onNext(T t);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment