From ba2eece6d5710f145fa40088f69980c2958fe281 Mon Sep 17 00:00:00 2001 From: orchsik Date: Wed, 7 Dec 2022 20:27:43 +0900 Subject: [PATCH 1/9] =?UTF-8?q?Add=202.=20=EA=B0=9D=EC=B2=B4=EC=A7=80?= =?UTF-8?q?=ED=96=A5=20=ED=94=84=EB=A1=9C=EA=B7=B8=EB=9E=98=EB=B0=8D=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4bb7c27..f022397 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,9 @@ 3. 대상이 비록 실세계에서는 생명이 없는 수동적인 존재라고 하더라도 객체지향의 세계로 넘어오는 순간 그들은 생명과 지능을 가진 싱싱한 존재로 다시 태어난다. 4. 훌륭한 객체지향 설계란 협력하는 객체 사이의 의존성을 적절하게 관리하는 설계다. 세상에 엮인 것이 많은 사람일수록 변하기 어려운 것처럼 객체가 실행되는 주변 환경에 강하게 결합될 수록 변경하기 어려워진다. +5. 어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지 고민하라. + 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야 한다. +6. 책임의 위치를 결정하기 위해 조건문을 사용하는 것은 협력의 설계 측면에서 대부분의 경우 좋지 않은 선택이다. 항상 예외 케이스를 최소화하고 일관성을 유지할 수 있는 방법을 선택하라. ## ⦿ 목차 * [1. 객체, 설계]( #1.-객체,-설계 ) @@ -31,4 +34,27 @@ 사실 관람객이 가방을 가지고 있다는 사실과 판매원이 매표소에서 티켓을 판매한다는 사실을 Theater가 알아야할 필요가 없다. Theater가 원하는 것은 관람객이 소극장에 입장하는 것뿐이다. 따라서 관람객이 스스로 가방 안의 현금과 초대장을 처리하고 판매원이 스스로 매표소의 티켓과 판매 요금을 다루게 한다면 이 모든 문제를 한 번에 해결할 수 있을 것 이다. - 다시 말해서 관람객과 판매원을 **자율적인 존재**로 만들면 되는 것이다. \ No newline at end of file + 다시 말해서 관람객과 판매원을 **자율적인 존재**로 만들면 되는 것이다. + +
+ +## 2. 객체지향 프로그래밍 +* [학습 코드](https://github.com/orchsik/study-object/pull/2) +* 클래스를 구현하거나 다른 개발자에 의해 개발된 클래스를 사용할 때 가장 중요한 것은 클래스의 경계를 구분 짓는 것이다. 클래스는 내부와 외부로 구분되며 훌륭한 클래스를 + 설계하기 위한 핵심은 어떤 부분을 외부에 공개하고 어떤 부분을 감출지를 결정하는 것이다. 클래스의 내부와 외부를 구분(구현 은닉)해야 하는 이유는 무엇일까? + * 경계의 명확성이 객체의 자율성을 보장하기 때문이다. + * 프로그래머에게 구현의 자유를 제공하기 때문이다. 클라이언트 프로그래머는 내부의 구현은 무시한 채 인터페이스만 알고 있어도 클래스를 사용할 수 있다. + 클래스 작성자는 인터페이스를 바꾸지 않는 한 외부에 미치는 영향을 걱정하지 않고도 내부 구현을 마음대로 변경할 수 있다. +* 협력(Collaboration) + * 객체가 다른 객체와 상호작용할 수 있는 유일한 방법은 **메시지를 전송(send a message)** 하는 것뿐이다. 다른 객채에게 요청이 도착할 때 해당 객체가 + **메시지를 수신(receive a message)** 했다고 이야기 한다. 메시지를 수신한 객체는 스스로의 결정에 따라 자율적으로 메시지를 처리할 방법을 결정한다. + 이 처럼 수신된 메시지를 처리하기 위한 자신만의 방법을 **메서드(method)** 라고 부른다. +* 다형성 + * 다형성은 메세지에 응답하기 위해 실행될 메서드를 컴파일 시점이 아닌 실행 시점에 결정한다는 공통점이 있다. + * 이를 **지연 바인딩(lazy binding)** 또는 동적 **바인딩(dynamic binding)** 이라고 부른다. <-> 초기 바인딩 또는 정적 바인딩. +* 부모 클래스에 기본적인 알고리즘의 흐름을 구현하고 중간에 필요한 처리를 자식에게 위임하는 디자인 패턴을 TEMPLATE METHOD 패턴이라고 부른다. + * DiscountPolicy 클래스의 calculateDiscountAmount 메서드, getDiscountAmount 메서드 구현을 위임하고 있다. +* 헙성(Composition) + * 합성은 상속이 가지는 두 가지 문제점을 모두 해결한다. 인터페이스에 정의된 메시지를 통해서만 재사용이 가능하기 때문에 구현을 효과적으로 캡슐화할 수 있다. + 또한 의존하는 인스턴스를 교체하는 것이 비교적 쉽기 때문에 설계를 유연하게 만든다. 상속은 클래스를 통해 강하게 결합되는 데 비해 합성은 메시지를 통해 느슨 + 하게 결합된다. 따라서 코드 재사용을 위해서는 상속보다는 합성을 선호하는 거싱 더 좋은 방법이다. \ No newline at end of file From 035694bfb47f2692b2d7a670e822f9b9c55ac2f3 Mon Sep 17 00:00:00 2001 From: orchsik Date: Wed, 7 Dec 2022 20:28:54 +0900 Subject: [PATCH 2/9] =?UTF-8?q?Add:=20=EA=B8=88=EC=95=A1=EC=9D=84=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=ED=95=98=EB=8A=94=20Money=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/orchsik/object/_02_movie/Money.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 app/src/main/java/com/orchsik/object/_02_movie/Money.java diff --git a/app/src/main/java/com/orchsik/object/_02_movie/Money.java b/app/src/main/java/com/orchsik/object/_02_movie/Money.java new file mode 100644 index 0000000..440486a --- /dev/null +++ b/app/src/main/java/com/orchsik/object/_02_movie/Money.java @@ -0,0 +1,45 @@ +package com.orchsik.object._02_movie; + +import java.math.BigDecimal; + +/** + * 금액과 관련된 다양한 계산을 구현하는 간단한 클래스 + */ +public class Money { + public static final Money ZERO = Money.wons(0); + + private final BigDecimal amount; + + public static Money wons(long amount) { + return new Money(BigDecimal.valueOf(amount)); + } + + public static Money wons(double amount) { + return new Money(BigDecimal.valueOf(amount)); + } + + Money(BigDecimal amount) { + this.amount = amount; + } + + public Money plus(Money amount) { + return new Money(this.amount.add(amount.amount)); + } + + public Money minus(Money amount) { + return new Money(this.amount.subtract(amount.amount)); + } + + public Money times(double percent) { + return new Money(this.amount.multiply(BigDecimal.valueOf(percent))); + } + + public boolean isLessThan(Money other) { + return amount.compareTo(other.amount) < 0; + } + + public boolean isGreaterThanOrEqual(Money other) { + return amount.compareTo(other.amount) >= 0; + } + +} From 445aaf516ced8b5385a8122a860a77dd84ae7fc2 Mon Sep 17 00:00:00 2001 From: orchsik Date: Wed, 7 Dec 2022 20:31:11 +0900 Subject: [PATCH 3/9] =?UTF-8?q?Add:=20=EC=83=81=EC=98=81=20=EC=98=81?= =?UTF-8?q?=ED=99=94=EB=A5=BC=20=EA=B5=AC=EB=A7=A4=ED=95=98=EB=8A=94=20Cus?= =?UTF-8?q?tomer=20=ED=81=B4=EB=9E=98=EC=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/orchsik/object/_02_movie/Customer.java | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 app/src/main/java/com/orchsik/object/_02_movie/Customer.java diff --git a/app/src/main/java/com/orchsik/object/_02_movie/Customer.java b/app/src/main/java/com/orchsik/object/_02_movie/Customer.java new file mode 100644 index 0000000..75f03dd --- /dev/null +++ b/app/src/main/java/com/orchsik/object/_02_movie/Customer.java @@ -0,0 +1,4 @@ +package com.orchsik.object._02_movie; + +public class Customer { +} From 1fb45cd3831f4df70adff944510d7786c3575a33 Mon Sep 17 00:00:00 2001 From: orchsik Date: Wed, 7 Dec 2022 21:22:03 +0900 Subject: [PATCH 4/9] =?UTF-8?q?Add:=20=ED=95=A0=EC=9D=B8=EC=A0=95=EC=B1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_02_movie/AmountDiscountPolicy.java | 16 ++++++++++ .../_02_movie/DefaultDiscountPolicy.java | 30 +++++++++++++++++++ .../object/_02_movie/DiscountPolicy.java | 5 ++++ .../object/_02_movie/NoneDiscountPolicy.java | 10 +++++++ .../_02_movie/PercentDiscountPolicy.java | 16 ++++++++++ 5 files changed, 77 insertions(+) create mode 100644 app/src/main/java/com/orchsik/object/_02_movie/AmountDiscountPolicy.java create mode 100644 app/src/main/java/com/orchsik/object/_02_movie/DefaultDiscountPolicy.java create mode 100644 app/src/main/java/com/orchsik/object/_02_movie/DiscountPolicy.java create mode 100644 app/src/main/java/com/orchsik/object/_02_movie/NoneDiscountPolicy.java create mode 100644 app/src/main/java/com/orchsik/object/_02_movie/PercentDiscountPolicy.java diff --git a/app/src/main/java/com/orchsik/object/_02_movie/AmountDiscountPolicy.java b/app/src/main/java/com/orchsik/object/_02_movie/AmountDiscountPolicy.java new file mode 100644 index 0000000..060c84d --- /dev/null +++ b/app/src/main/java/com/orchsik/object/_02_movie/AmountDiscountPolicy.java @@ -0,0 +1,16 @@ +package com.orchsik.object._02_movie; + +public class AmountDiscountPolicy extends DefaultDiscountPolicy { + private Money discountAmount; + + public AmountDiscountPolicy(Money discountAmount, DiscountCondition... conditions) { + super(conditions); + this.discountAmount = discountAmount; + } + + @Override + protected Money getDiscountAmount(Screening screening) { + return discountAmount; + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/orchsik/object/_02_movie/DefaultDiscountPolicy.java b/app/src/main/java/com/orchsik/object/_02_movie/DefaultDiscountPolicy.java new file mode 100644 index 0000000..9763684 --- /dev/null +++ b/app/src/main/java/com/orchsik/object/_02_movie/DefaultDiscountPolicy.java @@ -0,0 +1,30 @@ +package com.orchsik.object._02_movie; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * AmountPolicy, PercentPolicy로 두 가지 할인 정책 구현 + * 두 클래스는 대부분의 코드가 유사하고 할인 요금을 계산하는 방식만 조금 다르므로, DiscountPolicy 라는 부모 클래스 사용 + * 실제 애플리케이션에서는 DiscountPolicy의 인스턴스를 생성할 필요가 없기 때문에 추상 클래스로 구현 + */ +public abstract class DefaultDiscountPolicy implements DiscountPolicy { + private List conditions = new ArrayList<>(); + + public DefaultDiscountPolicy(DiscountCondition... conditions) { + this.conditions = Arrays.asList(conditions); + } + + public Money calculateDiscountAmount(Screening screening) { + for (DiscountCondition each : conditions) { + if (each.isSatisfiedBy(screening)) { + return getDiscountAmount(screening); + } + } + + return Money.ZERO; + } + + abstract protected Money getDiscountAmount(Screening screening); +} diff --git a/app/src/main/java/com/orchsik/object/_02_movie/DiscountPolicy.java b/app/src/main/java/com/orchsik/object/_02_movie/DiscountPolicy.java new file mode 100644 index 0000000..214a691 --- /dev/null +++ b/app/src/main/java/com/orchsik/object/_02_movie/DiscountPolicy.java @@ -0,0 +1,5 @@ +package com.orchsik.object._02_movie; + +public interface DiscountPolicy { + public Money calculateDiscountAmount(Screening screening); +} diff --git a/app/src/main/java/com/orchsik/object/_02_movie/NoneDiscountPolicy.java b/app/src/main/java/com/orchsik/object/_02_movie/NoneDiscountPolicy.java new file mode 100644 index 0000000..f33d203 --- /dev/null +++ b/app/src/main/java/com/orchsik/object/_02_movie/NoneDiscountPolicy.java @@ -0,0 +1,10 @@ +package com.orchsik.object._02_movie; + +public class NoneDiscountPolicy implements DiscountPolicy { + + @Override + public Money calculateDiscountAmount(Screening screening) { + return Money.ZERO; + } + +} diff --git a/app/src/main/java/com/orchsik/object/_02_movie/PercentDiscountPolicy.java b/app/src/main/java/com/orchsik/object/_02_movie/PercentDiscountPolicy.java new file mode 100644 index 0000000..37d4c4b --- /dev/null +++ b/app/src/main/java/com/orchsik/object/_02_movie/PercentDiscountPolicy.java @@ -0,0 +1,16 @@ +package com.orchsik.object._02_movie; + +public class PercentDiscountPolicy extends DefaultDiscountPolicy { + private double percent; + + public PercentDiscountPolicy(double percent, DiscountCondition... conditions) { + super(conditions); + this.percent = percent; + } + + @Override + protected Money getDiscountAmount(Screening screening) { + return screening.getMovieFee().times(percent); + } + +} From 4a4fcc4e660dfcd65ca6fbb54fb50f01935e1582 Mon Sep 17 00:00:00 2001 From: orchsik Date: Wed, 7 Dec 2022 21:22:53 +0900 Subject: [PATCH 5/9] =?UTF-8?q?Add:=20=ED=95=A0=EC=9D=B8=EC=A1=B0=EA=B1=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../object/_02_movie/DiscountCondition.java | 7 ++++ .../object/_02_movie/PeriodCondition.java | 32 +++++++++++++++++++ .../object/_02_movie/SequenceCondition.java | 18 +++++++++++ 3 files changed, 57 insertions(+) create mode 100644 app/src/main/java/com/orchsik/object/_02_movie/DiscountCondition.java create mode 100644 app/src/main/java/com/orchsik/object/_02_movie/PeriodCondition.java create mode 100644 app/src/main/java/com/orchsik/object/_02_movie/SequenceCondition.java diff --git a/app/src/main/java/com/orchsik/object/_02_movie/DiscountCondition.java b/app/src/main/java/com/orchsik/object/_02_movie/DiscountCondition.java new file mode 100644 index 0000000..cb05d00 --- /dev/null +++ b/app/src/main/java/com/orchsik/object/_02_movie/DiscountCondition.java @@ -0,0 +1,7 @@ +package com.orchsik.object._02_movie; + +public interface DiscountCondition { + + public boolean isSatisfiedBy(Screening screening); + +} diff --git a/app/src/main/java/com/orchsik/object/_02_movie/PeriodCondition.java b/app/src/main/java/com/orchsik/object/_02_movie/PeriodCondition.java new file mode 100644 index 0000000..eb54b27 --- /dev/null +++ b/app/src/main/java/com/orchsik/object/_02_movie/PeriodCondition.java @@ -0,0 +1,32 @@ +package com.orchsik.object._02_movie; + +import java.time.DayOfWeek; +import java.time.LocalTime; + +/** + * @dayOfWeek - 요일 + * @startTime - 시작 시간 + * @endTime - 종료 시간 + */ +public class PeriodCondition implements DiscountCondition { + private DayOfWeek dayOfWeek; + private LocalTime startTime; + private LocalTime endTime; + + public PeriodCondition(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) { + this.dayOfWeek = dayOfWeek; + this.startTime = startTime; + this.endTime = endTime; + } + + /** + * 상영 요일이 dayOfWeek와 같고 상영 시작 시간이 startTime과 endTime 사이에 있을 경우 true + */ + @Override + public boolean isSatisfiedBy(Screening screening) { + return screening.getStarTime().getDayOfWeek().equals(dayOfWeek) + && startTime.compareTo(screening.getStarTime().toLocalTime()) <= 0 + && endTime.compareTo(screening.getStarTime().toLocalTime()) >= 0; + } + +} diff --git a/app/src/main/java/com/orchsik/object/_02_movie/SequenceCondition.java b/app/src/main/java/com/orchsik/object/_02_movie/SequenceCondition.java new file mode 100644 index 0000000..750e6fa --- /dev/null +++ b/app/src/main/java/com/orchsik/object/_02_movie/SequenceCondition.java @@ -0,0 +1,18 @@ +package com.orchsik.object._02_movie; + +/** + * 할인 여부를 판단하기 위해 사용할 순번(sequence)을 인스턴스 변수로 포함 + */ +public class SequenceCondition implements DiscountCondition { + private int sequence; + + public SequenceCondition(int sequence) { + this.sequence = sequence; + } + + @Override + public boolean isSatisfiedBy(Screening screening) { + return screening.isSequence(sequence); + } + +} From caa8bfbead2d30c486f57b6a514872b8f6df37b2 Mon Sep 17 00:00:00 2001 From: orchsik Date: Wed, 7 Dec 2022 21:23:44 +0900 Subject: [PATCH 6/9] =?UTF-8?q?Add:=20=EC=98=81=ED=99=94=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EB=A5=BC=20=ED=91=9C=ED=98=84=ED=95=98=EB=8A=94=20Mov?= =?UTF-8?q?ie=20=ED=81=B4=EB=9E=98=EC=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/orchsik/object/_02_movie/Movie.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 app/src/main/java/com/orchsik/object/_02_movie/Movie.java diff --git a/app/src/main/java/com/orchsik/object/_02_movie/Movie.java b/app/src/main/java/com/orchsik/object/_02_movie/Movie.java new file mode 100644 index 0000000..992985c --- /dev/null +++ b/app/src/main/java/com/orchsik/object/_02_movie/Movie.java @@ -0,0 +1,33 @@ +package com.orchsik.object._02_movie; + +import java.time.Duration; + +public class Movie { + private String title; + private Duration runningTime; + private Money fee; + private DiscountPolicy discountPolicy; + + public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) { + this.title = title; + this.runningTime = runningTime; + this.fee = fee; + this.discountPolicy = discountPolicy; + } + + public Money getFee() { + return fee; + } + + public Money calculateMovieFee(Screening screening) { + return fee.minus(discountPolicy.calculateDiscountAmount(screening)); + } + + /** + * 실행 시점에 할인 정책을 간단하게 변경할 수 있다 + */ + public void changeDiscountPolicy(DiscountPolicy discountPolicy) { + this.discountPolicy = discountPolicy; + } + +} From cb3362d7c0b6f09b7e081a238776efa87fd0a72f Mon Sep 17 00:00:00 2001 From: orchsik Date: Wed, 7 Dec 2022 21:24:35 +0900 Subject: [PATCH 7/9] =?UTF-8?q?Add:=20=EC=83=81=EC=98=81=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EB=A5=BC=20=EB=82=98=ED=83=80=EB=82=B4=EB=8A=94=20Scr?= =?UTF-8?q?eening=20=20=ED=81=B4=EB=9E=98=EC=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../orchsik/object/_02_movie/Screening.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 app/src/main/java/com/orchsik/object/_02_movie/Screening.java diff --git a/app/src/main/java/com/orchsik/object/_02_movie/Screening.java b/app/src/main/java/com/orchsik/object/_02_movie/Screening.java new file mode 100644 index 0000000..507443a --- /dev/null +++ b/app/src/main/java/com/orchsik/object/_02_movie/Screening.java @@ -0,0 +1,49 @@ +package com.orchsik.object._02_movie; + +import java.time.LocalDateTime; + +/** + * 사용자들이 예매하는 대상인 '상영'을 구현한다. + * 상영할 영화(movie), 순번(sequence), 상영시작시간(whenScreened) 를 인스턴스 변수로 포함. + */ +public class Screening { + private Movie movie; + private int sequence; + private LocalDateTime whenScreened; + + public Screening(Movie movie, int sequence, LocalDateTime whenScreened) { + this.movie = movie; + this.sequence = sequence; + this.whenScreened = whenScreened; + } + + public LocalDateTime getStarTime() { + return whenScreened; + } + + public boolean isSequence(int sequence) { + return this.sequence == sequence; + } + + public Money getMovieFee() { + return movie.getFee(); + } + + /** + * @param customer - 예매자 정보 + * @param audienceCount - 인원수 + * @return - 영화를 예매한 후 예매 정보를 담고 있는 Reservation의 인스턴스를 생성해서 반환 + */ + public Reservation reserve(Customer customer, int audienceCount) { + return new Reservation(customer, this, calcuateFee(audienceCount), audienceCount); + } + + /** + * @param audienceCount + * @return - 1인당 예매 요금 + */ + private Money calcuateFee(int audienceCount) { + return movie.calculateMovieFee(this).times(audienceCount); + } + +} From d62458dae94852c89f54d3b60a04b5d62ab2bf4c Mon Sep 17 00:00:00 2001 From: orchsik Date: Wed, 7 Dec 2022 21:25:08 +0900 Subject: [PATCH 8/9] =?UTF-8?q?Add:=20=EC=98=88=EC=95=BD=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EB=A5=BC=20=EB=82=98=ED=83=80=EB=82=B4=EB=8A=94=20Res?= =?UTF-8?q?ervation=20=20=ED=81=B4=EB=9E=98=EC=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../orchsik/object/_02_movie/Reservation.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 app/src/main/java/com/orchsik/object/_02_movie/Reservation.java diff --git a/app/src/main/java/com/orchsik/object/_02_movie/Reservation.java b/app/src/main/java/com/orchsik/object/_02_movie/Reservation.java new file mode 100644 index 0000000..1853d2a --- /dev/null +++ b/app/src/main/java/com/orchsik/object/_02_movie/Reservation.java @@ -0,0 +1,16 @@ +package com.orchsik.object._02_movie; + +public class Reservation { + private Customer customer; + private Screening screening; + private Money fee; + private int audienceCount; + + public Reservation(Customer customer, Screening screening, Money fee, int audienceCount) { + this.customer = customer; + this.screening = screening; + this.fee = fee; + this.audienceCount = audienceCount; + } + +} From b4c0262a70ae86694e15c8cd890267114aa8c032 Mon Sep 17 00:00:00 2001 From: orchsik Date: Wed, 7 Dec 2022 21:26:45 +0900 Subject: [PATCH 9/9] =?UTF-8?q?Test:=20Movie=20=ED=81=B4=EB=9E=98=EC=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../orchsik/object/_02_movie/MovieTest.java | 389 ++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 app/src/test/java/com/orchsik/object/_02_movie/MovieTest.java diff --git a/app/src/test/java/com/orchsik/object/_02_movie/MovieTest.java b/app/src/test/java/com/orchsik/object/_02_movie/MovieTest.java new file mode 100644 index 0000000..ced4313 --- /dev/null +++ b/app/src/test/java/com/orchsik/object/_02_movie/MovieTest.java @@ -0,0 +1,389 @@ +package com.orchsik.object._02_movie; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.time.*; +import java.util.List; +import java.util.stream.Collectors; + +@SuppressWarnings({ "InnerClassMayBeStatic", "NonAsciiCharacters" }) +@DisplayName("Movie 클래스") +class MovieTest { + private final Money given_고정할인금액 = Money.wons(800); + private final double given_할인비율 = 0.1; + private final LocalDateTime given_월요일 = LocalDate.of(2020, Month.MARCH, 2).atStartOfDay(); + private final LocalDateTime given_화요일 = given_월요일.plusDays(1); + private final LocalDateTime given_목요일 = given_월요일.plusDays(3); + private final LocalDateTime given_일요일 = given_월요일.plusDays(6); + private final LocalDateTime given_일요일_오후 = given_일요일.withHour(13).withMinute(30); + + Movie given_아바타() { + return new Movie( + "아바타", + Duration.ofMinutes(120), + Money.wons(10000), + given_아바타_할인정책); + } + + private final DiscountPolicy given_아바타_할인정책 = new AmountDiscountPolicy( + given_고정할인금액, + new SequenceCondition(1), + new SequenceCondition(10), + new PeriodCondition(DayOfWeek.MONDAY, LocalTime.of(10, 0), LocalTime.of(11, 59)), + new PeriodCondition(DayOfWeek.THURSDAY, LocalTime.of(10, 0), LocalTime.of(20, 59))); + + Movie given_타이타닉() { + return new Movie( + "타이타닉", + Duration.ofMinutes(180), + Money.wons(11000), + given_타이타닉_할인정책); + } + + private final DiscountPolicy given_타이타닉_할인정책 = new PercentDiscountPolicy(0.1, + new PeriodCondition(DayOfWeek.TUESDAY, LocalTime.of(14, 0), LocalTime.of(16, 59)), + new SequenceCondition(2), + new PeriodCondition(DayOfWeek.THURSDAY, LocalTime.of(10, 0), LocalTime.of(13, 59))); + + Movie given_스타워즈() { + return new Movie( + "스타워즈", + Duration.ofMinutes(210), + Money.wons(10000), + given_스타워즈_할인정책); + } + + private final DiscountPolicy given_스타워즈_할인정책 = new NoneDiscountPolicy(); + + abstract class TestCalculateMovieFee { + abstract Movie givenMovie(); + + Money 기본_요금() { + return givenMovie().getFee(); + } + + Money subject(Screening screening) { + return givenMovie().calculateMovieFee(screening); + } + } + + @Nested + @DisplayName("calculateMovieFee 메소드는") + class Describe_calculateMovieFee { + @Nested + @DisplayName("주어진 영화가 '아바타'일때 (할인 조건: 상영 시작 시간, 상영 순번 / 할인 금액: 고정 금액)") + class Context_with_avatar extends TestCalculateMovieFee { + Movie givenMovie() { + return given_아바타(); + } + + @Nested + @DisplayName("상영 시작 시간이 할인 조건에 맞는다면") + class Context_with_valid_period { + final List 할인_조건에_맞는_상영_시작_시간들 = List.of( + // edge cases - 월요일 + given_월요일.withHour(10).withMinute(0), + given_월요일.withHour(11).withMinute(59), + // inner cases - 월요일 + given_월요일.withHour(10).withMinute(1), + given_월요일.withHour(11).withMinute(58), + // edge cases - 목요일 + given_목요일.withHour(10).withMinute(0), + given_목요일.withHour(20).withMinute(59), + // inner cases - 목요일 + given_목요일.withHour(10).withMinute(1), + given_목요일.withHour(11).withMinute(58)); + + List givenScreens() { + return 할인_조건에_맞는_상영_시작_시간들.stream() + .map(상영시간 -> new Screening(givenMovie(), 0, 상영시간)) + .collect(Collectors.toList()); + } + + @Test + @DisplayName("고정할인 금액만큼 할인된 금액을 리턴한다") + void it_returns_discounted_fee() { + for (Screening 할인되는_시간에_시작하는_상영 : givenScreens()) { + final Money 계산된_요금 = subject(할인되는_시간에_시작하는_상영); + + Assertions.assertEquals(기본_요금().minus(given_고정할인금액), 계산된_요금); + } + } + } + + @Nested + @DisplayName("상영 시작 시간이 할인 조건에 맞지 않는다면") + class Context_with_invalid_period { + final List 할인_조건에_맞지_않는_상영_시작_시간들 = List.of( + // 월요일 + given_월요일.withHour(9).withMinute(59), + given_월요일.withHour(12).withMinute(0), + // 목요일 + given_목요일.withHour(9).withMinute(59), + given_목요일.withHour(21).withMinute(0), + // 그 외의 요일 + given_화요일.withHour(10).withMinute(0), + given_화요일.withHour(10).withMinute(1), + given_화요일.withHour(10).withMinute(30)); + + List givenScreens() { + return 할인_조건에_맞지_않는_상영_시작_시간들.stream() + .map(상영시간 -> new Screening(givenMovie(), -1, 상영시간)) + .collect(Collectors.toList()); + } + + @Test + @DisplayName("할인되지 않은 금액을 리턴한다") + void it_returns_fee_does_not_discounted() { + for (Screening 할인되는_시간에_시작하는_상영 : givenScreens()) { + final Money 계산된_요금 = subject(할인되는_시간에_시작하는_상영); + + Assertions.assertEquals(기본_요금(), 계산된_요금); + } + } + } + + @Nested + @DisplayName("상영 순번이 할인 조건에 맞는다면") + class Context_with_valid_seq { + final List 지정된_상영_순번 = List.of(1, 10); + + List givenScreens() { + return 지정된_상영_순번.stream() + .map(seq -> new Screening(givenMovie(), seq, given_일요일_오후)) + .collect(Collectors.toList()); + } + + @Test + @DisplayName("고정할인 금액만큼 할인된 금액을 리턴한다") + void it_returns_discounted_fee() { + for (Screening 할인되는_순번의_상영 : givenScreens()) { + final Money 계산된_요금 = subject(할인되는_순번의_상영); + + Assertions.assertEquals(기본_요금().minus(given_고정할인금액), 계산된_요금); + } + } + } + + @Nested + @DisplayName("상영 순번이 할인 조건에 맞지 않는다면") + class Context_with_invalid_seq { + final List 지정되지_않은_상영_순번 = List.of(2, 9); + + List givenScreens() { + return 지정되지_않은_상영_순번.stream() + .map(seq -> new Screening(givenMovie(), seq, given_일요일_오후)) + .collect(Collectors.toList()); + } + + @Test + @DisplayName("할인되지 않은 금액을 리턴한다") + void it_returns_fee_not_discounted() { + for (Screening 상영 : givenScreens()) { + final Money 계산된_요금 = subject(상영); + + Assertions.assertEquals(기본_요금(), 계산된_요금); + } + } + } + } + + @Nested + @DisplayName("주어진 영화가 '타이타닉'일때 (할인 조건: 상영 시작 시간, 상영 순번 / 할인 금액: 퍼센트)") + class Context_with_titanic extends TestCalculateMovieFee { + Movie givenMovie() { + return given_타이타닉(); + } + + @Nested + @DisplayName("상영 시작 시간이 할인 조건에 맞는다면") + class Context_with_valid_period { + final List 지정된_기간_내의_시간들 = List.of( + // edge cases - 화요일 + given_화요일.withHour(14).withMinute(0), + given_화요일.withHour(16).withMinute(59), + // inner cases - 화요일 + given_화요일.withHour(14).withMinute(1), + given_화요일.withHour(16).withMinute(58), + // edge cases - 목요일 + given_목요일.withHour(10).withMinute(0), + given_목요일.withHour(13).withMinute(59), + // inner cases - 목요일 + given_목요일.withHour(10).withMinute(1), + given_목요일.withHour(13).withMinute(58)); + + List givenScreens() { + return 지정된_기간_내의_시간들.stream() + .map(상영시간 -> new Screening(givenMovie(), 0, 상영시간)) + .collect(Collectors.toList()); + } + + @Test + @DisplayName("지정된 비율만큼 할인된 금액을 리턴한다") + void it_returns_discounted_fee() { + for (Screening 할인되는_시간에_시작하는_상영 : givenScreens()) { + final Money 계산된_요금 = subject(할인되는_시간에_시작하는_상영); + + Assertions.assertEquals(기본_요금().times(1 - given_할인비율), 계산된_요금); + } + } + } + + @Nested + @DisplayName("상영 시작 시간이 할인 조건에 맞지 않는다면") + class Context_with_invalid_period { + final List 할인_조건에_맞지_않는_상영_시작_시간들 = List.of( + // 월요일 + given_월요일.withHour(9).withMinute(59), + given_월요일.withHour(12).withMinute(0), + // 목요일 + given_목요일.withHour(9).withMinute(59), + given_목요일.withHour(21).withMinute(0), + // 그 외의 요일 + given_화요일.withHour(10).withMinute(0), + given_화요일.withHour(10).withMinute(1), + given_화요일.withHour(10).withMinute(30)); + + List givenScreens() { + return 할인_조건에_맞지_않는_상영_시작_시간들.stream() + .map(상영시간 -> new Screening(givenMovie(), -1, 상영시간)) + .collect(Collectors.toList()); + } + + @Test + @DisplayName("할인되지 않은 금액을 리턴한다") + void it_returns_fee_not_discounted() { + for (Screening 할인되는_시간에_시작하는_상영 : givenScreens()) { + final Money 계산된_요금 = subject(할인되는_시간에_시작하는_상영); + + Assertions.assertEquals(기본_요금(), 계산된_요금); + } + } + } + + @Nested + @DisplayName("상영 순번이 할인 조건에 맞는다면") + class Context_with_valid_seq { + final List 지정된_상영_순번 = List.of(2); + + List givenScreens() { + return 지정된_상영_순번.stream() + .map(seq -> new Screening(givenMovie(), seq, given_일요일_오후)) + .collect(Collectors.toList()); + } + + @Test + @DisplayName("지정된 비율만큼 할인된 금액을 리턴한다") + void it_returns_discounted_fee() { + for (Screening 할인되는_순번의_상영 : givenScreens()) { + final Money 계산된_요금 = subject(할인되는_순번의_상영); + + Assertions.assertEquals(기본_요금().times(1 - given_할인비율), 계산된_요금); + } + } + } + + @Nested + @DisplayName("상영 순번이 할인 조건에 맞지 않는다면") + class Context_with_invalid_seq { + final List 지정되지_않은_상영_순번 = List.of(1, 3, 4, 5, 6, 7, 8, 9, 10); + + List givenScreens() { + return 지정되지_않은_상영_순번.stream() + .map(seq -> new Screening(givenMovie(), seq, given_일요일_오후)) + .collect(Collectors.toList()); + } + + @Test + @DisplayName("할인되지 않은 금액을 리턴한다") + void it_returns_fee_not_discounted() { + for (Screening 상영 : givenScreens()) { + final Money 계산된_요금 = subject(상영); + + Assertions.assertEquals(기본_요금(), 계산된_요금); + } + } + } + } + + @Nested + @DisplayName("주어진 영화가 '스타워즈'일때 (할인 조건 없음)") + class Context_with_starwars extends TestCalculateMovieFee { + Movie givenMovie() { + return given_스타워즈(); + } + + @Test + @DisplayName("할인되지 않은 금액을 리턴한다") + void it_returns_fee_not_discounted() { + final Screening 상영 = new Screening(givenMovie(), 0, given_일요일_오후); + final Money 계산된_요금 = subject(상영); + + Assertions.assertEquals(기본_요금(), 계산된_요금, "할인되지 않았으므로 금액은 변함이 없다"); + } + } + } + + @Nested + @DisplayName("changeDiscountPolicy 메소드는") + class Describe_changeDiscountPolicy { + @Nested + @DisplayName("주어진 영화가 '스타워즈'일때 (할인 조건 없음)") + class Context_with_starwars { + Movie givenMovie() { + return given_스타워즈(); + } + + @Nested + @DisplayName("'아바타'의 할인정책이 주어지면") + class Context_with_avatar_discount_policy { + final DiscountPolicy given_할인정책 = given_아바타_할인정책; + + @Test + @DisplayName("주어진 할인 정책으로 할인정책을 교체하고 void를 리턴한다") + void it_changes_the_discount_policy() { + final Movie 스타워즈 = givenMovie(); + final Money 기본_요금 = 스타워즈.getFee(); + 스타워즈.changeDiscountPolicy(given_할인정책); + + { + /* 변경된 할인 정책으로 할인이 되는지 확인한다. */ + final int 아바타_할인_조건_순번 = 1; + final Screening 상영 = new Screening(스타워즈, 아바타_할인_조건_순번, given_일요일_오후); + final Money 계산된_요금 = 스타워즈.calculateMovieFee(상영); + + Assertions.assertEquals(기본_요금.minus(given_고정할인금액), 계산된_요금, + "할인되지 않는 스타워즈의 요금이 아바타의 정책으로 할인된다"); + } + } + } + + @Nested + @DisplayName("'타이타닉'의 할인정책이 주어지면") + class Context_with_starwars_policy { + final DiscountPolicy given_할인정책 = given_타이타닉_할인정책; + + @Test + @DisplayName("주어진 할인 정책으로 할인정책을 교체하고 void를 리턴한다") + void it_changes_the_discount_policy() { + final Movie movie = givenMovie(); + final Money 기본_요금 = movie.getFee(); + movie.changeDiscountPolicy(given_할인정책); + + { + /* 변경된 할인 정책으로 할인이 되는지 확인한다. */ + final int 타이타닉_할인_조건_순번 = 2; + final Screening 상영 = new Screening(movie, 타이타닉_할인_조건_순번, given_일요일_오후); + final Money 계산된_요금 = movie.calculateMovieFee(상영); + + Assertions.assertEquals(기본_요금.times(1 - given_할인비율), 계산된_요금, + "할인되지 않는 스타워즈의 요금이 타이타닉의 정책으로 할인된다"); + } + } + } + } + } +} \ No newline at end of file