Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
cfb3c72
docs: add feature list
junslog Nov 12, 2023
d7f9b86
feat: add skeleton code for application
junslog Nov 13, 2023
61aa904
feat: add feature to print statement for user to insert reservation day
junslog Nov 13, 2023
ba4d32b
feat: add feature to check user input about reservation day is in app…
junslog Nov 13, 2023
31990b9
feat: add feature for user to insert reservation day
junslog Nov 13, 2023
c3cafdf
feat: add feature to check user input about reservation day is not empty
junslog Nov 13, 2023
851a890
fix: fix error when invalid input about reservation day is inserted
junslog Nov 13, 2023
f941f33
feat: add feature to remove blank in user input about reservation day
junslog Nov 13, 2023
ef06b19
feat: add feature to check user input length about reservation day is…
junslog Nov 13, 2023
221e83b
feat: add feature to check user input about reservation day is conver…
junslog Nov 13, 2023
0e47ef0
feat: add feature to check user input about reservation day is positi…
junslog Nov 13, 2023
5fbe307
docs: rearrange feature list
junslog Nov 13, 2023
a9aca2e
feat: add feature to print statement for user to insert orders
junslog Nov 13, 2023
d864f3c
refactor: indicate method handling exception throws specific type of …
junslog Nov 13, 2023
00f7558
feat: add feature for user to insert orders
junslog Nov 13, 2023
c904585
docs: add constraint to user input about orders
junslog Nov 13, 2023
4d89124
feat: add feature to check user input about orders is in appropriate …
junslog Nov 13, 2023
47ee9c2
docs: add constraint to user input about orders
junslog Nov 13, 2023
863c565
feat: add feature to remove blank in user input about orders
junslog Nov 13, 2023
49830ad
feat: add feature to check user input about order is not empty
junslog Nov 13, 2023
b1616ac
docs: update feature list
junslog Nov 13, 2023
245b009
feat: add feature to check all user input length is under limit(2000)…
junslog Nov 13, 2023
6e68e48
feat: add feature to check user input about order contains delimiter(…
junslog Nov 13, 2023
55ba4f0
feat: add feature to check user input about menu name length is in ap…
junslog Nov 13, 2023
937363f
refactor: change input constant values to be enum
junslog Nov 13, 2023
5745b52
refactor: change empty input notification to contain information abou…
junslog Nov 13, 2023
4d81ca6
feat: add feature to check user input about menu count is not empty
junslog Nov 13, 2023
fa00100
feat: add feature to check user input about menu count length is not …
junslog Nov 13, 2023
f011be9
feat: add feature to check user input about menu name exists in menu …
junslog Nov 14, 2023
8f75c94
docs: change constraint about error message format to meet requirement
junslog Nov 14, 2023
add32f1
refactor: change error message format to meet requirement
junslog Nov 14, 2023
c2e1fe8
fix: fix problem that exception is not thrown when menu name input is…
junslog Nov 14, 2023
71152b8
feat: add feature to check user input about menu count is convertible…
junslog Nov 14, 2023
87333c1
feat: add feature to check user input about menu count is positive in…
junslog Nov 14, 2023
143d6da
feat: add feature to check user input about orders does not have dupl…
junslog Nov 14, 2023
1e257cd
feat: add feature to check sum of ordered menus does not exceed upper…
junslog Nov 14, 2023
a381715
feat: add feature to check user input about ordered menus does not on…
junslog Nov 14, 2023
c218e24
feat: add feature to calculate total price of ordered menus before di…
junslog Nov 14, 2023
60c4395
feat: add feature to judge the given day is Christmas D-Day Promotion…
junslog Nov 14, 2023
2430ccf
refactor: split createOrders method to make code more easy to read
junslog Nov 14, 2023
87a82ae
feat: add feature to judge the given day is Weekend Promotion applica…
junslog Nov 14, 2023
7b7d2fa
feat: add feature to judge the given day is Weekday Promotion applica…
junslog Nov 14, 2023
e2b3449
feat: add feature to judge the given day is Special Promotion applica…
junslog Nov 14, 2023
106f652
refactor: divide one controller to many controller to make domains mo…
junslog Nov 14, 2023
8eb9fca
feat: add feature to determine whether event is applicable to the tot…
junslog Nov 14, 2023
80b5feb
feat: add feature to determine whether gift event is applicable to th…
junslog Nov 14, 2023
948ccbd
docs: update feature list to have new feature that application print …
junslog Nov 14, 2023
4c03076
feat: add feature to print breif intro message when result starts to …
junslog Nov 14, 2023
9b76e0d
feat: add feature to print statement notifying about ordered menus
junslog Nov 14, 2023
f043836
feat: add feature to print ordered menu using appropriate format
junslog Nov 14, 2023
1113983
feat: add feature to print statement notifying about total amount wit…
junslog Nov 14, 2023
16d37bd
refactor: rename class name DecemberDay to ReservationDay to imply it…
junslog Nov 14, 2023
1da8219
feat: add feature to print total amount with no discount using approp…
junslog Nov 14, 2023
b37453a
test: add test to check ReservationDay domain logic
junslog Nov 14, 2023
8e776f6
test: add test to check Order domain logic
junslog Nov 14, 2023
b05d593
test: add test to check Orders domain logic
junslog Nov 14, 2023
a5f6302
feat: add feature to print gift menu using appropriate format
junslog Nov 14, 2023
b991761
feat: add feature to create benefitsDetails using Orders and Reservat…
junslog Nov 14, 2023
dbd05d4
refactor: rename classes about ReservationDay from name containing Da…
junslog Nov 14, 2023
0ad1b1a
feat: add feature to print benefits details using appropriate format
junslog Nov 14, 2023
6fe1558
fix: fix error that weekday or weekend promotion is applied even when…
junslog Nov 14, 2023
2dde13c
refactor: move EventService code to EventManager domain to impose res…
junslog Nov 14, 2023
3fd1824
feat: add feature to calculate total benefited amount
junslog Nov 14, 2023
dbc6031
feat: add feature to print total benefited amount using appropriate f…
junslog Nov 14, 2023
3c7847b
feat: add feature to calculate total discounted amount
junslog Nov 14, 2023
b17acb1
feat: add feature to calculate estimated amount with discount
junslog Nov 14, 2023
f587dcf
feat: add feature to print estimated amount with discount using appro…
junslog Nov 14, 2023
8608a60
feat: add feature to issue event badge based on total benefited amount
junslog Nov 14, 2023
81231c9
feat: add feature to print event badge using appropriate format
junslog Nov 14, 2023
c01933d
refactor: make console to be closed when event planner is terminated.
junslog Nov 15, 2023
44b0835
refactor: make Collections unmodifiable to prevent from unexpected ch…
junslog Nov 15, 2023
336dd75
refactor: remove NONE EventBadge and make NO_BADGE_SYMBOL to shift re…
junslog Nov 15, 2023
a257e23
refactor: indicate explicitly that what kind of exception can method …
junslog Nov 15, 2023
75fd7dc
refactor: move responsibility of output of total benefited amount fro…
junslog Nov 15, 2023
e7944ad
refactor: make ApplicationConstant finalized and remove unnecessary f…
junslog Nov 15, 2023
e46096b
refactor: remove redundunt ERROR_PREFIX addition in exception messages
junslog Nov 15, 2023
c4805f3
test: add test to check EventManager domain logic
junslog Nov 15, 2023
039a1b1
test: add test to check Menu domain logic
junslog Nov 15, 2023
1899ecf
test: add test to check EventBadge domain logic
junslog Nov 15, 2023
f48428a
test: add test to check ChristmasPromotion domain logic
junslog Nov 15, 2023
0fd035a
refactor: extract duplicated part to method in EventManagerTest
junslog Nov 15, 2023
d97bae5
test: add test code for InputParser
junslog Nov 15, 2023
ce6e107
test: add test code for DayInputValidator
junslog Nov 15, 2023
3f98a76
test: add test code for OrdersInputValidator
junslog Nov 15, 2023
c8ba2ef
test: add test code for MenuNameInputValidator
junslog Nov 15, 2023
22557d6
test: add test code for MenuCountInputValidator
junslog Nov 15, 2023
067626c
test: add test code for OrderInputValidator
junslog Nov 15, 2023
3571d80
refactor: remove magic literal in test code
junslog Nov 15, 2023
6acfacf
docs: update README to explain about application
junslog Nov 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
614 changes: 614 additions & 0 deletions docs/README.md
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 본 리드미 중에 가장 완성형에 가깝습니다.

독자가 비개발자, 개발자인걸 고려해서 윗단은 비즈니스팀 아랫단은 개발자를 위한 내용을 잘 작성하셨습니다.

순서 또한 to가 비즈니스팀인걸 생각하셔서 제일 윗단에 작성하신것도 정말 좋네요

그리고 목차까지 있어서 원하는 내용도 바로 숏컷으로 볼 수 있어서 참 편한 것 같습니다.

결론은 전체적으로 잘 작성된 리드미 입니다! 👍

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions src/main/java/christmas/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package christmas;

import christmas.view.input.InputView;
import christmas.view.output.OutputView;

public class Application {
public static void main(String[] args) {
// TODO: 프로그램 구현
EventPlanner eventPlanner = new EventPlanner(new InputView(), new OutputView());
eventPlanner.execute();
Comment on lines +8 to +9
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EventPlanner 객체를 컨트롤러 성향이 아닌, main과 같은 레벨로 두신 점이 인상깊었습니다.

Application.main과 EventPlanner를 구분하신 이유가 있을까요?
어떤 장점을 얻을 수 있을 지 궁금합니다.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Application은 최상단의 클래스로,
EventPlanner의 생성과 실행시키는 것을 담당합니다.
자원의 생성 및 실행, 관리 및 설정을 총괄한다 생각합니다. ( 추후 Application에서 모든 클래스에 대한 의존성 주입을 해주는 것도 고려하고 있습니다. )

EventPlanner는 말 그대로 플래너 서비스 그 자체입니다.
이 서비스가 단일로 존재하면 Application과 큰 차이가 없겠지만
추후 다른 서비스가 추가되는 경우를 생각하면 Application과 EventPlanner를 구분해놓은 것이 의미가 있을 것이라 생각합니다!

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@junslog 추후에 어떤 서비스가 어떻게 추가되는 걸 상정하셨나요?

Copy link
Copy Markdown
Owner Author

@junslog junslog Dec 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@junslog 추후에 어떤 서비스가 어떻게 추가되는 걸 상정하셨나요?

이벤트 결과를 다 보여주고 난 뒤에 주문 데이터를 받아와서 주문으로 연결해주는 서비스를 예시로 들 수 있을 거 같습니다만
이렇게 얘기하는건, 미리 상정했다고 말씀 못드리겠습니다.

말씀 듣고 고민해보니 '확장성'이라는 가능성을 열어두면서 구현하는건 좋으나,
확장성 자체에 초점을 맞춰서 구현을 하는건 그리 좋은 방식이 아니라 생각이 듭니다.

확장성을 핑계로 구조를 틀어버린 느낌..?? 사실 누군가 제 코드에 대한 질문을 할 때,
"확장성을 고려했어요~ "하면 되게 뭉뚱그릴 수 있는 답변으로 무언가 중요한걸 회피하는거 아닐까? 하는 생각이 듭니다.
개발 시 확장성을 우선순위가 1순위로 두는게 아니라, 2~3순위로 고려해야하는 것 같다는 느낌이 드네요.

좋은 개발은 뭘까?를 생각을 더 해보게 되는 말씀이었습니다. 감사합니다!

}
}
}
82 changes: 82 additions & 0 deletions src/main/java/christmas/EventPlanner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package christmas;

import camp.nextstep.edu.missionutils.Console;
import christmas.controller.OrdersController;
import christmas.controller.ReservationDayController;
import christmas.domain.EventManager;
import christmas.domain.Orders;
import christmas.domain.ReservationDay;
import christmas.service.EventManagerService;
import christmas.view.input.InputView;
import christmas.view.output.OutputView;

public class EventPlanner {
private final OutputView outputView;
private final ReservationDayController reservationDayController;
private final OrdersController ordersController;
private final EventManagerService eventManagerService;

public EventPlanner(InputView inputView, OutputView outputView) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어떤 인스턴스 변수는 파라미터로 받고 있고,
어떤 인스턴스 변수는 생성자 내부에서 객체를 생성하고 있네요
어떤 기준으로 이렇게 나누셨나요?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inputViewoutputView는 최초 한번 생성되게 하고
제어의 흐름에 따라 미리 생성된 InputView, outputView를 사용하게 하려 하였습니다.
그 외의 인스턴스 변수는 객체가 생성될 때 새로 생성하려 하였습니다.

inputView, outtputView는 생성비용이 많이 든다 생각하여 싱글톤으로 구현해야한다 생각했고,
오버엔지니어링을 지적해주셔서 살짝 말씀드리기 고민되지만
멀티 쓰레드 환경에서 여러 사용자가 사용할 때, 해당 사용자를 위한 서비스, 컨트롤러들을 생성하려는 생각을 가지고 구현하였습니다.

this.outputView = outputView;
reservationDayController = new ReservationDayController(inputView, outputView);
ordersController = new OrdersController(inputView, outputView);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

컨트롤러를 두 개를 쓰신 이유가 궁금합니다. 그리고 두 개의 컨트롤러가 하나의 인풋뷰를 관리하는 것이 조금 낯선데, 이렇게 구성하신 이유가 있나요?

그리고 제 개인적인 생각으로는 이 EventPlanner가 제일 컨트롤러 역할에 가까운 느낌입니다.
(뷰와 서비스 계층을 이어준다는 느낌에서)

Copy link
Copy Markdown
Owner Author

@junslog junslog Nov 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 Controller 란, UI도메인을 연결해주는 통로라고 생각했습니다.

말씀해주신대로 EventPlanner 라는 클래스가 메인 컨트롤러의 역할을 하고,
각 UI 별 로직을 담당 컨트롤러를 두어
메인 컨트롤러는 지휘자의 느낌,
서브 컨트롤러들은 각 파트의 장 느낌으로 계층을 구성하려는 의도였어요!

물론 이 흐름대로면, 하나의 InputView를 관리하는 것이 아닌 각 도메인마다 뷰가 달라야하겠지만, 입력받는 Input값의 수가 많은 편이 아니고, 보여줘야할 데이터는 많지만 각 서브 도메인들의 협업으로 처리한 결과물 ( 이벤트 결과 )을 보여주는 로직이 많다 생각해서
뷰를 쪼개지는 않았습니다!

eventManagerService = new EventManagerService();
outputView.printGreetingMessage();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

생성 시 메세지가 출력되도록 한 것 신선해요 ㅎㅎㅎ

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

execute() 메서드에서 바로 예약날짜와 주문을 입력받는 코드를 보여줘서 가독성을 높이는 것이 의도였는데,
생성자에서 할만한 일인가? 하는 생각이 들더라고요.
가독성을 챙기려다 확장성에서 좀 아쉬운 판단을 한거같다는 갠적인 생각합니다..ㅎㅎ

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

굿굿 생성자에서 빼주시는게 더 좋을 것 같습니다.

}
Comment on lines +19 to +25
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

생성자에서 많은 일을 하는 것 같습니다! 출력같은 부분은 다른 메서드에서 하는것도 좋을 것 같아요!

추가로 다른 필드값을 파라미터로 받아 외부에서 주입하는 방식은 어떨까요?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

execute() 부분의 코드를 간결하게 짜고 싶은 욕심에
생성자에 출력 로직을 부여하였습니다. 이 부분은 생성자의 역할이 아니라 생각이 드네요.

또한, 의존성 주입 방법 정말 괜찮은 것 같습니다.
new 로 생성되는 것이 클래스 군데군데 퍼져있으면 관리하기 어려울 것 같아요.
좋은 의견 감사합니다!


public void execute() {
ReservationDay reservationDay = reservationDayController.insertReservationDay();
Orders orders = ordersController.insertOrders();
EventManager eventManager = EventManager.of(reservationDay, orders);
printResult(reservationDay, orders, eventManager);
terminatePlanner();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

execute 메서드 안에 terminatePlanner() 로직이 들어있는게 이해가 가는데
(저도 이렇게 했다가 이 클래스 외부 - Application.main() 에서 처리하자로 바꿨어서요)
자원 정리 느낌이라 어디에 두어도 상관은 없을 것 같기는 합니당!

Copy link
Copy Markdown
Owner Author

@junslog junslog Nov 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀대로 돌아보니,
Application에서 EventPlanner 시스템만 시행할 것이라는 전제가 깔린 코드라는 생각이 듭니다.
혹여 다른 시스템이 추가될 미래를 생각한다면, Application 내에 종료될 때 자원을 회수하는 것이 좋겠어요!

}

private void printResult(ReservationDay reservationDay, Orders orders, EventManager eventManager) {
printIntroMessage(reservationDay);
printOrderedMenus(orders);
printTotalAmountWithNoDiscount(orders);
printGiftMenu(orders);
printBenefitsDetails(eventManager);
printTotalBenefitedAmount(eventManager);
printEstimatedAmountWithDiscount(eventManager);
printEventBadge(eventManager);
Comment on lines +36 to +43
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이걸 OutputView 에서 printResult() 라는 통합 메서드가 관리하도록 하고 컨트롤러는 outputView.printResult() 를 호출하도록 했어도 좋았을 것 같아요.(이렇게 바꿀 경우 파라미터도 수정을 해야하고 그렇지만...) 뷰의 출력 순서가 컨트롤러에 노출된 느낌이 들어서요. 어떻게 하는게 더 좋을지는 모르겠습니당ㅎㅎ

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 사실 이 부분에 대한 트레이드 오프가 있다고 생각해요.

컨트롤러가 뷰의 출력 순서를 안다는 것은, 자칫하면 컨트롤러가 복잡해질 수도 있지만, 세밀한 제어 및 유지보수가 가능하다는 장점이 있다 생각했습니다.

또한 뷰를 아예 다 갈아엎는 것보다, '특정' 뷰의 조각에 대한 요구사항 변화가 잦기에, 뷰를 만드는 로직은 쪼개져야한다 생각했습니다.

또한, SOLID 원칙의 ISP 원칙으로 보았을 때, OutputView의 클라이언트는 Controller이고, Controlller에게 뷰에 대해 독립된 인터페이스를 제공하는 것이 추후 유지보수를 위해 좋다 판단하여 이렇게 구현하였습니다!

물론 어떤 원칙이 답이니까 그렇게 해야해! 는 아니지만, 아직까지는 그래도 분리함으로써 얻는 이익이 코스트보다 크다고 생각하고 있습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 해당 메서드가 controller에 있어서 어떤 식으로 세밀한 제어 및 유지보수가 가능할까요?
  2. 해당 메서드를 OutputView에 public으로 선언하고,
    내부 메서드들은 private으로 만들었을 때는 어떤 문제가 있을까요?
  3. ISP 원칙에서 말하는 인터페이스란 무엇인가요?

Copy link
Copy Markdown
Owner Author

@junslog junslog Dec 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 해당 메서드가 controller에 있어서 어떤 식으로 세밀한 제어 및 유지보수가 가능할까요?
  2. 해당 메서드를 OutputView에 public으로 선언하고,
    내부 메서드들은 private으로 만들었을 때는 어떤 문제가 있을까요?
  3. ISP 원칙에서 말하는 인터페이스란 무엇인가요?
  1. 세밀하게 유지보수하는걸 컨트롤러의 책임으로 넘겼다는 생각이 듭니다.
    이건 outputView가 알아서 잘 해줘야할 부분이란 생각이 드네요.

  2. 문제가 없습니다. 오히려 컨트롤러에서 알아야할 정보의 복잡성을 줄여주는 좋은 코드가 될 것 같습니다.

  3. 이 부분에 대해 계속 고민했는데, 아직 이것이 뭐다! 라고 명쾌하게 말할 수는 없는 거 같습니다..
    다만 제가 이해하고 있는 인터페이스의 개념은 아래로 정리할 수 있습니다.

인터페이스란 결국 '내가 제공하는 기능들의 사용자 그룹들에게 제공하는 기능 목록' 이라 생각합니다.
ISP란 사용자 그룹 A~Z와 같이 다양한 사용자 그룹이 있다고 가정할 떄,
각 사용자 그룹에 최적화된 인터페이스를 각기 다르게 분리하여 제공해주는 것이라 생각합니다.
( 마치 데이터베이스의 뷰 개념처럼 )

일단 느끼는 것은,
컨트롤러가 다양하게 제어를 할 수 있는 길을 열어줬다 생각하는 것 = 다양한 API를 제공해주면 세밀하게 제어가 가능하니 좋은거겠지! 했던것이
사실은 컨트롤러가 몰라도 되는, 알면 오히려 더 피곤해지는 정보들을 와다다 준 것 같다는 생각이 듭니다.☹️

}

private void printIntroMessage(ReservationDay reservationDay) {
outputView.printIntroMessage(reservationDayController.createEventBenefitsPreviousDto(reservationDay));
}

private void printOrderedMenus(Orders orders) {
outputView.printOrderedMenus(ordersController.createOrderedMenusDto(orders));
}

private void printTotalAmountWithNoDiscount(Orders orders) {
outputView.printTotalAmountWithNoDiscount(ordersController.createTotalAmountWithNoDiscountDto(orders));
}

private void printGiftMenu(Orders orders) {
outputView.printGiftMenu(eventManagerService.createGiftDto(orders));
}

private void printBenefitsDetails(EventManager eventManager) {
outputView.printBenefitsDetails(eventManagerService.createBenefitsDetailsDto(eventManager));
}

private void printTotalBenefitedAmount(EventManager eventManager) {
outputView.printTotalBenefitedAmount(eventManagerService.createTotalBenefitedAmountDto(eventManager));
}

private void printEstimatedAmountWithDiscount(EventManager eventManager) {
outputView.printEstimatedAmountWithDiscount(
eventManagerService.createEstimatedAmountWithDiscountDto(eventManager));
}

private void printEventBadge(EventManager eventManager) {
outputView.printEventBadge(eventManagerService.createEventBadgeDto(eventManager));
}
Comment on lines +46 to +77
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OutputView 가 필요로 하는 정보를 DTO로 잘게 쪼개서 전달하고 있는데,
이 DTO 들을 묶는 큰 DTO가 있어도 좋을 것 같아요.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#1 (comment)

해당 의견에 대해 언급드린 이 부분의 의견과 동일합니다!


private void terminatePlanner() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

프로그램의 종료를 명시해주니 더 깔끔한 느낌입니다.

Console.close();
}
}
47 changes: 47 additions & 0 deletions src/main/java/christmas/controller/OrdersController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package christmas.controller;

import christmas.domain.Orders;
import christmas.domain.exception.InvalidOrdersException;
import christmas.dto.OrderedMenusDto;
import christmas.dto.TotalAmountWithNoDiscountDto;
import christmas.service.OrdersService;
import christmas.view.input.InputView;
import christmas.view.input.exception.BasicInputException;
import christmas.view.input.exception.OrdersInputException;
import christmas.view.output.OutputView;
import java.util.Map;

public class OrdersController {
private final InputView inputView;
private final OutputView outputView;
private final OrdersService ordersService;

public OrdersController(InputView inputView, OutputView outputView) {
this.inputView = inputView;
this.outputView = outputView;
ordersService = new OrdersService();
}

public Orders insertOrders() {
try {
Map<String, Integer> orders = askToInsertOrders();
return ordersService.createOrders(orders);
} catch (BasicInputException | OrdersInputException | InvalidOrdersException e) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

람다문 내부 변수에 대해서도 e가 아니라 예컨데 exception과 같이 구체적인 변수 명으로 선언하면 더 좋을 것 같아요!

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

커스텀 예외 클래스 모두 IllegalArgumentException 을 상속 받으면 catch 문이 더 간단해 질 것 같아요! �이후 다른 커스텀 예외가 추가되는 상황에 대비하기도 좋을 것 같습니다!

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

명시적으로 어떤 예외가 발생할 수 있는지에 대해 기술하려 하였습니다!
물론.. 다른 커스텀 예외가 발생하면, 좀 일이 커질 것 같네요.
그걸 생각해보니... 확실히 IllegalArgumentException으로 통일해서 받는게 좋은 방법 같습니다 :)
감사합니다!

outputView.printErrorMessage(e.getMessage());
return insertOrders();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

재입력 받는 로직을 재귀로 처리하는 것도 좋다고 생각합니다. 다만 가능성이 낮다고는 해도 스택 오버플로우에 대한 대책이 있어야 할 것 같아요.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 저도 이 부분이.. 스택이 계속 쌓여서 터질 수 있겠다 생각했었습니다.
이 부분을 while 문을 통한 반복으로 구현하는 것이 좋을 것 같다 생각합니다.

또한, 공통된 함수 형식인 부분을 함수형 인터페이스로 따로 뺄 수 있더라고요!

private <T> T readUserInput(Supplier<T> supplier) {
        while (true) {
            try {
                return supplier.get();
            } catch (IllegalArgumentException e) {
                outputView.printError(e.getMessage());
            }
        }
    }
return readUserInput(() -> {
            int userInput = ~
            ...
            return reservationday...
        });

이와 같이 람다를 사용할 수 있더군요!

Comment on lines +25 to +31
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while - try - catch 구문이 날짜를 입력 받을때도 쓰이고 있는데, 이 중복되는 부분을 함수형 인터페이스를 이용해서 템플릿 형태로 사용하시면 어떨까요??

@FunctionalInterface
public interface InputCallback<T> {

    T run() throws IllegalArgumentException;
}
public class InputTemplate {

    public <T> T execute(InputCallback<T> callback) {
        while (true) {
            try {
                return callback.run();
            } catch (IllegalArgumentException e) {
                System.out.println(e.getMessage());
            }
        }
    }
}

이런식으로 템플릿은 함수를 매개변수로 받아서 해당 함수를 실행하게끔 하면 됩니다!

Copy link
Copy Markdown
Owner Author

@junslog junslog Nov 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

함수형 인터페이스를 정말 잘 활용하시네요!

입력부분에서 반복되는 코드를 짜는게 참 기분이 안좋았는데

이런 좋은 방법이 있다는걸 알았다면..ㅎ 지금부터라도 적용해봐야겠습니다. 감사합니다!!!

}
}

private Map<String, Integer> askToInsertOrders() throws BasicInputException, OrdersInputException {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

메뉴 입력 시 이름, 수량 외에 다른 값이 추가된다면 Map<K, V> 사용이 힘들 거 같습니다! DTO 클래스는 어떨까요?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헉 지금까지 Input Data로 여러 개의 데이터를 받아오는 경우가 없다보니
싹 다 원시값 또는 컬렉션 자료구조로 커버했었습니다.

Dto로 만들면 여러 개의 input값을 한번에 담을 수 있고,
input값에 의미있는 이름을 명시할 수 있어서 좋을 것 같네요. 좋은 의견 감사합니다 :>

outputView.askToInsertOrders();
return inputView.getOrders();
}

public OrderedMenusDto createOrderedMenusDto(Orders orders) {
return ordersService.createOrdersHistoryDto(orders);
}

public TotalAmountWithNoDiscountDto createTotalAmountWithNoDiscountDto(Orders orders) {
return ordersService.createTotalAmountWithNoDiscountDto(orders);
}
Comment on lines +40 to +46
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요런 DTO 로의 변환 로직을 컨트롤러에 추가한 이유가 궁금합니당 :)
(ReservationDayController 의 createEventBenefitsPreviousDto() 와 동일하게요)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DTO 내부에서 도메인 객체에 대한 변환을 하게 되면,
도메인 객체가 하나 변하는 순간, 그로부터 파생되는 DTO는 다양할 것이기에 변경의 전파범위가 작지 않다 생각했습니다.
서비스 단에서 결국 DTO들을 생성하기에, 서비스단 에 변환로직을 둔다면 추후 유지보수성에서 유리하다 판단했습니다.

}
41 changes: 41 additions & 0 deletions src/main/java/christmas/controller/ReservationDayController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package christmas.controller;

import christmas.domain.ReservationDay;
import christmas.domain.exception.InvalidReservationDayException;
import christmas.dto.EventBenefitsPreviewDto;
import christmas.service.ReservationDayService;
import christmas.view.input.InputView;
import christmas.view.input.exception.BasicInputException;
import christmas.view.input.exception.DayInputException;
import christmas.view.output.OutputView;

public class ReservationDayController {
private final InputView inputView;
private final OutputView outputView;
private final ReservationDayService reservationDayService;

public ReservationDayController(InputView inputView, OutputView outputView) {
this.inputView = inputView;
this.outputView = outputView;
reservationDayService = new ReservationDayService();
}

public ReservationDay insertReservationDay() {
try {
int reservationDay = askToInsertReservationDay();
return reservationDayService.createReservationDay(reservationDay);
} catch (BasicInputException | DayInputException | InvalidReservationDayException e) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예외를 이렇게 나눠서 딱딱 적어주니 나중에 추적하기도 편할 것 같아요

outputView.printErrorMessage(e.getMessage());
return insertReservationDay();
}
}
Comment on lines +23 to +31
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

윗분이 말씀하신대로 예외가 상황별로 분류되어 있어 추후 문제가 발생했을때 트러블슈팅하기 편할것같네요 ㅎㅎ 이 부분은 메모해두었다가 저도 사용해보겠습니다!


private int askToInsertReservationDay() throws BasicInputException, DayInputException {
outputView.askToInsertReservationDay();
return inputView.getReservationDay();
}

public EventBenefitsPreviewDto createEventBenefitsPreviousDto(ReservationDay reservationDay) {
return reservationDayService.createEventBenefitsPreviewDto(reservationDay);
}
Comment on lines +38 to +40
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

도메인을 DTO로 변환하는 건 서비스에만 놔둬도 괜찮았을 것 같기도 해요 :)
컨트롤러는 뷰와 모델을 이어주는 역할만 하면 될 것 같아서요
여기에 이 메서드를 추가한 이유가 궁금해요 !!

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EventPlanner가 알아야하는 인터페이스의 범위를 컨트롤러단으로 한정시키고 싶었습니다.

물론 Controller에서의 메서드와 Service의 메서드 이름이 같고, 하는 일이 동일하지만

추후에 서비스에 다양한 기능이 생기고, 그 기능을 묶어서 한번에 제공하는 기능을 컨트롤러가 제공할 때,

메인 컨트롤러는 서브 컨트롤러의 API만 참고한다는 생각으로 해당 코드를 추가했습니다!

}
22 changes: 22 additions & 0 deletions src/main/java/christmas/domain/DayPerMonth.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package christmas.domain;

import static christmas.domain.constant.reservationday.ReservationDayConstant.DECEMBER_LAST_DAY;
import static christmas.domain.constant.reservationday.ReservationDayConstant.DEFAULT_FIRST_DAY;
import static christmas.domain.exception.message.InvalidReservationDayExceptionMessage.NOT_IN_APPROPRIATE_RANGE;

import christmas.domain.exception.InvalidReservationDayException;

public abstract class DayPerMonth {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

abtsract class 를 사용하신 이유가 궁금합니다.

Copy link
Copy Markdown
Owner Author

@junslog junslog Nov 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추후 1월, 2월 등 차후 이벤트 날짜에 대한 제약사항의 틀을 제시하기 위함입니다!

public DayPerMonth() {
}
Comment on lines +10 to +11
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기본 생성자를 별도로 만드신 이유가 있을까요? 생성자가 없으면 컴파일러가 기본 생성자를 만들어줍니다!

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 이 부분은 원래 생성자 내에서 검증 로직을 처리하는 것을 넣어두었다가
리팩터링하면서 바깥으로 뻈었는데, 그 이후로 잔재로 남아있던 것 같습니다.
짚어주셔서 감사합니다!!


protected void validate(final int day) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어떤것을 검증하는지 메서드명으로 표현하면 좋을 것 같아요!

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validateDayRange() 와 같은 이름으로 지을 수 있었겠네요.
좋은 조언 감사합니다!!!

if (!isInAppropriateRange(day)) {
throw InvalidReservationDayException.of(NOT_IN_APPROPRIATE_RANGE.getMessage());
}
}

protected boolean isInAppropriateRange(final int day) {
return day >= DEFAULT_FIRST_DAY.getValue() && day <= DECEMBER_LAST_DAY.getValue();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

표기법상 부정접두사 In-이 아닌 전치사 In으로 쓰인 것 같은데
!와 더해져서 혼동을 주는 것 같습니다.

Copy link
Copy Markdown
Owner Author

@junslog junslog Nov 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!isInAppropriateRange()

음.. 다시 보니 좀 실수할 수 있겠다는 생각이 드네요, 어떻게 이름을 지어야할지 고민되네요.

}
138 changes: 138 additions & 0 deletions src/main/java/christmas/domain/EventManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package christmas.domain;

import static christmas.domain.constant.event.ChristmasPromotion.CHRISTMAS_D_DAY_PROMOTION;
import static christmas.domain.constant.event.EventNumberConstant.NONE_PROMOTION_APPLIED_AMOUNT;
import static christmas.domain.constant.event.Promotion.GIFT_PROMOTION;
import static christmas.domain.constant.event.Promotion.SPECIAL_PROMOTION;
import static christmas.domain.constant.event.Promotion.WEEKDAY_PROMOTION;
import static christmas.domain.constant.event.Promotion.WEEKEND_PROMOTION;

import christmas.domain.constant.event.EventBadge;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

public class EventManager {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 클래스에서 하는 일을 조금 분리해서 계산같은 로직은 다른 클래스를 이용해도 좋았을 것 같습니다.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네, 객체에게 메시지를 보내서 일을 다 시킨다는 것만 생각했어서 객체에게 책임을 너무 무겁게 준 것 같습니다.
계산이나 할인 로직을 util 형식으로 빼서 구현을 해도 좋았겠다는 생각이 드네요 ㅠ

private final ReservationDay reservationDay;
private final Orders orders;

private EventManager(ReservationDay reservationDay, Orders orders) {
this.reservationDay = reservationDay;
this.orders = orders;
}

public static EventManager of(ReservationDay reservationDay, Orders orders) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

생성자랑 하는 일이 동일한데 굳이 정적 팩터리 메서드를 써야할까요?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분 고민해보았는데,
정적 팩터리 메서드를 쓰는 의미가 없는 것 같습니다.
지금까지 써보고, 공부하며 느꼈던 정적 팩터리 메서드는

  1. 메서드 이름을 통해 생성의 로직에 대한 직관적 유추 가능

  2. 생성 로직, 방식에 따라 다른 이름을 제공해서 생성자 오버로딩 / 빌더 패턴보다 나은 객체 생성에 대한 분리 및 명확성

이 두가지인데, 말씀해주신 코드에서 제가 쓴 방식은 그저 of라는 이름을 써서 생성하는 것 이외에 다른 의미가 없는 것 같습니다.

return new EventManager(reservationDay, orders);
}

public Map<String, Integer> createBenefitsDetails() {
Map<String, Integer> benefitsDetails = new LinkedHashMap<>();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LinkedHashMap을 사용한 이유가 궁금합니당~!

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혜택 출력 순서를 제가 의도한 순서로 유지해주기 위해 사용하였습니다!
물론 요구사항에서는 출력순서는 상관 없었지만, 제 욕심이었어요..!

if (orders.isEventApplicable()) {
return makeBenefitsDetails(benefitsDetails);
}
return Collections.unmodifiableMap(benefitsDetails);
}

private Map<String, Integer> makeBenefitsDetails(Map<String, Integer> benefitsDetails) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Map<String, Integer> benefitsDetails 로 넘겨주는게 아니라 객체를 생성해서 List<benefitDetail> 요런 식으로 넘겨주었어도 좋았을 것 같아요 :)
여러 메서드에서 자주 사용되는 것 같아보여서요

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

도메인 내에서 DTO를 생성하는 것에 대한 고민이 있었습니다.

실제 다양한 요구사항이 남발하여 1주마다 새로운 버전이 릴리즈 되는 현장이라면,

코어 로직에 해당하는 도메인의 변화보다, 뷰에 대한 요구사항 변화가 다양할 것이라 생각했습니다.

뷰에 대한 요구사항이 변한다는 것은, 그에 해당하는 출력 DTO의 스펙이 변한다는 것이고,

도메인 객체가 DTO의 생성에 관여하여 결합되는 순간 도메인 객체의 수정이 잦게 일어날 것이고

이는 또다시 이 도메인으로부터 파생된 DTO의 스펙을 변화시키며 코드를 수정할 것이 매우 많아질 것이라 판단했습니다.

그렇기에 도메인은 DTO의 생성에 대해 몰라야한다 생각했습니다.

하지만, 그럼 도메인의 모든 상태변수에 대해 getter를 두어, 서비스 단에서 DTO를 조립하게 하는 것은 좋지 않은 설계라 생각했습니다.

그러면, 위 두가지 문제의 타협점으로 최소한 도메인이 제공할 수 있는 데이터의 스펙을 정해서 서비스에 제공한 뒤,

서비스에서 이를 재조립하는 식으로 구현하면 된다 생각하여, 이와 같이 Map을 반환하도록 구현하였습니다!

혹시 @jisu-om 님의 생각은 어떠실까요?

addChristmasPromotionDetails(benefitsDetails);
addWeekdayPromotionDetails(benefitsDetails);
addWeekendPromotionDetails(benefitsDetails);
addSpecialPromotionDetails(benefitsDetails);
addGiftPromotionDetails(benefitsDetails);
return Collections.unmodifiableMap(benefitsDetails);
}

private void addChristmasPromotionDetails(Map<String, Integer> benefitsDetails) {
if (reservationDay.isChristmasPromotionApplicable()) {
benefitsDetails.put(CHRISTMAS_D_DAY_PROMOTION.getName(),
calculateChristmasPromotionBenefitAmount());
}
}

private int calculateChristmasPromotionBenefitAmount() {
return CHRISTMAS_D_DAY_PROMOTION.getBenefitAmount(reservationDay.getDay());
}

private void addWeekdayPromotionDetails(Map<String, Integer> benefitsDetails) {
if (canAddWeekdayPromotion()) {
benefitsDetails.put(WEEKDAY_PROMOTION.getName(),
calculateWeekdayPromotionBenefitAmount());
}
}

private boolean canAddWeekdayPromotion() {
return reservationDay.isWeekdayPromotionApplicable()
&& calculateWeekdayPromotionBenefitAmount()
> NONE_PROMOTION_APPLIED_AMOUNT.getValue();
}

private int calculateWeekdayPromotionBenefitAmount() {
int count = orders.getOrders().stream()
.filter(Order::isWeekdayPromotionApplicable)
.mapToInt(Order::getMenuCount)
.sum();
return WEEKDAY_PROMOTION.getBenefitAmount() * count;
}

private void addWeekendPromotionDetails(Map<String, Integer> benefitsDetails) {
if (canAddWeekendPromotion()) {
benefitsDetails.put(WEEKEND_PROMOTION.getName(),
calculateWeekendPromotionBenefitAmount());
}
}

private boolean canAddWeekendPromotion() {
return reservationDay.isWeekendPromotionApplicable()
&& calculateWeekendPromotionBenefitAmount()
> NONE_PROMOTION_APPLIED_AMOUNT.getValue();
}

private int calculateWeekendPromotionBenefitAmount() {
int count = orders.getOrders().stream()
.filter(Order::isWeekendPromotionApplicable)
.mapToInt(Order::getMenuCount)
.sum();
return WEEKEND_PROMOTION.getBenefitAmount() * count;
}

private void addSpecialPromotionDetails(Map<String, Integer> benefitsDetails) {
if (reservationDay.isSpecialPromotionApplicable()) {
benefitsDetails.put(SPECIAL_PROMOTION.getName(), calculateSpecialPromotionBenefitAmount());
}
}

private int calculateSpecialPromotionBenefitAmount() {
return SPECIAL_PROMOTION.getBenefitAmount();
}

private void addGiftPromotionDetails(Map<String, Integer> benefitsDetails) {
if (orders.isGiftEventApplicable()) {
benefitsDetails.put(GIFT_PROMOTION.getName(), calculateGiftPromotionBenefitAmount());
}
}

private int calculateGiftPromotionBenefitAmount() {
return GIFT_PROMOTION.getBenefitAmount();
}

public int calculateTotalBenefitedAmount() {
return createBenefitsDetails().values().stream()
.mapToInt(Integer::intValue)
.sum();
}

public int calculateEstimatedOrdersAmountWithDiscount() {
return orders.calculateTotalAmountWithNoDiscount() - calculateTotalDiscountedAmount();
}

private int calculateTotalDiscountedAmount() {
if (orders.isGiftEventApplicable()) {
return calculateTotalBenefitedAmount() - calculateGiftPromotionBenefitAmount();
}
return calculateTotalBenefitedAmount();
}

public String issueEventBadge() {
return EventBadge.getBadgeNameByBenefitedPrice(calculateTotalBenefitedAmount());
}
}
43 changes: 43 additions & 0 deletions src/main/java/christmas/domain/Order.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package christmas.domain;

import christmas.domain.constant.orders.FoodType;
import christmas.domain.constant.orders.Menu;
import christmas.domain.exception.InvalidOrdersException;

public class Order {
private final Menu menu;
private final int count;

private Order(Menu menu, final int count) {
this.menu = menu;
this.count = count;
}

public static Order of(final String menuName, final int count) throws InvalidOrdersException {
return new Order(Menu.searchByName(menuName), count);
}

public String getMenuName() {
return menu.getName();
}

public int getMenuCount() {
return count;
}

public FoodType getFoodType() {
return menu.getFoodType();
}

public boolean isWeekdayPromotionApplicable() {
return menu.isWeekDayPromotionApplicable();
}

public boolean isWeekendPromotionApplicable() {
return menu.isWeekendPromotionApplicable();
}

public int getTotalPrice() {
return menu.getPrice() * count;
}
}
Loading