Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[산타] 미니언 미션 제출합니다. #13

Open
wants to merge 2 commits into
base: gusah009-main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions kotlin-christmas/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ repositories {

dependencies {
implementation("com.github.woowacourse-projects:mission-utils:1.1.0")
/**
* mockito를 사용하면 any! 타입이 반환되어 NPE가 터짐. 모킹을 너무 사용하고 싶기 때문에 라이브러리 추가...
*/
testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
}

java {
Expand Down
11 changes: 10 additions & 1 deletion kotlin-christmas/src/main/kotlin/christmas/Application.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
package christmas

import christmas.controller.ChristmasController
import christmas.service.ChristmasService
import christmas.view.InputView
import christmas.view.OutputView

fun main() {
TODO("프로그램 구현")
ChristmasController.run(
InputView,
OutputView,
ChristmasService
)
}
22 changes: 22 additions & 0 deletions kotlin-christmas/src/main/kotlin/christmas/badge/Badge.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package christmas.badge

import christmas.model.Price
import christmas.validation.ChristmasException

enum class Badge(val minBenefitAmount: UInt) {

Choose a reason for hiding this comment

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

한글 객체 직관적이고 재미있네요 ㅎㅎ
저도 다음에 직관성이 필요하거나, 귀찮은(?) 요구사항이 있으면 한글로 만들어봐야겠습니다 ⚡️

별(5_000u),
트리(10_000u),
산타(20_000u),
;

companion object {
fun of(benefitAmount: Price) =
when (benefitAmount.value.toUInt()) {
in 0u until 별.minBenefitAmount -> null
in 별.minBenefitAmount until 트리.minBenefitAmount -> 별
in 트리.minBenefitAmount until 산타.minBenefitAmount -> 트리
in 산타.minBenefitAmount until UInt.MAX_VALUE -> 산타
else -> throw ChristmasException("말도 안되는 혜택 금액입니다. 혜택 금액: $benefitAmount")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package christmas.controller

import christmas.service.ChristmasService
import christmas.view.InputView
import christmas.view.OutputView

object ChristmasController {

Choose a reason for hiding this comment

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

object에 대해 이리저리 서칭하면서 생각해봤습니다.

스프링-코틀린에서는 단순히 모듈성 기능을 하는, 즉 상태가 필요하지 않은 경우 object를 사용하여 싱글턴으로 관리할 수 있을 것 같아요. (미니언님이 만든 InputView, OutputView 처럼요!)

또한 말씀대로 object를 사용하면 인스턴스를 생성하는 코드를 따로 적지않아도 관리되는 측면에선 좋은 것 같으나, 생성 흐름을 제어하지 못한다는 점에서 단점이 있을 수 있을 것 같습니다. 따라서 인스턴스의 생명주기에 대한 흐름을 제어할 필요가 없다면 충분히 object를 사용해도 좋은 것 같다고 생각합니다!

fun run(
inputView: InputView,
outputView: OutputView,
christmasService: ChristmasService,
) {
outputView.printWelcomeMessage()
val userOrderInfo = inputView.getUserOrderInfo()
val benefitInfo = christmasService.getBenefitInfo(userOrderInfo)
outputView.printUserEventBenefit(userOrderInfo, benefitInfo)
}
}
111 changes: 111 additions & 0 deletions kotlin-christmas/src/main/kotlin/christmas/event/EventPolicy.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package christmas.event

import christmas.menu.Menu
import christmas.menu.MenuType
import christmas.menu.MenuWithCount
import christmas.model.Price
import christmas.model.UserOrderInfo
import org.assertj.core.util.VisibleForTesting
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.Month

private const val MIN_EVENT_PRICE = 10_000

enum class EventPolicy {
GiveawayEventPolicy {
override fun isOwnSupported(userOrderInfo: UserOrderInfo) = userOrderInfo.totalPrice >= 120_000

override fun getBenefit(userOrderInfo: UserOrderInfo): EventType =
EventType.Giveaway(MenuWithCount(Menu.샴페인, 1), this.getPolicyName())

override fun getPolicyName() = "증정 이벤트"
},

ChristmasDiscountPolicy {
override fun isOwnSupported(userOrderInfo: UserOrderInfo) =
userOrderInfo.estimatedVisitDate.isBefore(LocalDate.of(2023, 12, 26))

override fun getBenefit(userOrderInfo: UserOrderInfo) =
EventType.Discount(
Price(1000 + 100 * (userOrderInfo.estimatedVisitDate.dayOfMonth - 1)),
this.getPolicyName()
)

override fun getPolicyName() = "크리스마스 디데이 할인"
},

WeekdayDiscountPolicy {
private val discountPricePerMenu = 2023
private val availableDiscountDayOfWeek = listOf(
DayOfWeek.MONDAY,
DayOfWeek.TUESDAY,
DayOfWeek.WEDNESDAY,
DayOfWeek.THURSDAY,
DayOfWeek.FRIDAY
)

override fun isOwnSupported(userOrderInfo: UserOrderInfo) =
availableDiscountDayOfWeek.contains(userOrderInfo.estimatedVisitDate.dayOfWeek)

override fun getBenefit(userOrderInfo: UserOrderInfo) =
EventType.Discount(
Price(userOrderInfo.getSumPriceOfMenu(MenuType.Dessert) * discountPricePerMenu),
this.getPolicyName()
)

override fun getPolicyName() = "평일 할인"
},

WeekendDiscountPolicy {
private val discountPricePerMenu = 2023
private val availableDiscountDayOfWeek = listOf(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY)

override fun isOwnSupported(userOrderInfo: UserOrderInfo) =
availableDiscountDayOfWeek.contains(userOrderInfo.estimatedVisitDate.dayOfWeek)

override fun getBenefit(userOrderInfo: UserOrderInfo) =
EventType.Discount(
Price(userOrderInfo.getSumPriceOfMenu(MenuType.Main) * discountPricePerMenu),
this.getPolicyName()
)

override fun getPolicyName() = "주말 할인"
},

SpecialDiscountPolicy {
private val discountPrice = 1000
private val availableDiscountDay = listOf(
LocalDate.of(2023, 12, 3),
LocalDate.of(2023, 12, 10),
LocalDate.of(2023, 12, 17),
LocalDate.of(2023, 12, 24),
LocalDate.of(2023, 12, 25),
LocalDate.of(2023, 12, 31),
)

override fun isOwnSupported(userOrderInfo: UserOrderInfo) =
availableDiscountDay.contains(userOrderInfo.estimatedVisitDate)

override fun getBenefit(userOrderInfo: UserOrderInfo) =
EventType.Discount(Price(discountPrice), this.getPolicyName())

override fun getPolicyName() = "특별 할인"
},
;

fun isSupported(userOrderInfo: UserOrderInfo) =
userOrderInfo.totalPrice >= MIN_EVENT_PRICE &&
isInEventDate(userOrderInfo) &&
isOwnSupported(userOrderInfo)

private fun isInEventDate(userOrderInfo: UserOrderInfo) =
userOrderInfo.estimatedVisitDate.year == 2023 && userOrderInfo.estimatedVisitDate.month == Month.DECEMBER

@VisibleForTesting
internal abstract fun isOwnSupported(userOrderInfo: UserOrderInfo): Boolean

abstract fun getBenefit(userOrderInfo: UserOrderInfo): EventType

protected abstract fun getPolicyName(): String
}
Copy link

@KIJUNG-CHAE KIJUNG-CHAE Mar 4, 2024

Choose a reason for hiding this comment

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

enum 클래스로 각 할인정책을 공통적으로 관리하는 방법이 있었네요!
접근 제한을 둔 부분도 좋은 것 같습니다.
배워갑니다 👍🏻

16 changes: 16 additions & 0 deletions kotlin-christmas/src/main/kotlin/christmas/event/EventType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package christmas.event

import christmas.menu.MenuWithCount
import christmas.model.Price

sealed class EventType(
private val benefitAmount: Price,
private val eventPolicyName: String
) {
class Discount(val price: Price, eventPolicyName: String) : EventType(price, eventPolicyName)

class Giveaway(val menuWithCount: MenuWithCount, eventPolicyName: String) :
EventType(menuWithCount.benefitAmount, eventPolicyName)

override fun toString() = "${eventPolicyName}: ${benefitAmount.toMinusString()}"
}
34 changes: 34 additions & 0 deletions kotlin-christmas/src/main/kotlin/christmas/menu/Menu.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package christmas.menu

import christmas.model.Price

enum class Menu(val price: Price, val menuType: MenuType) {
양송이스프(Price(6_000), MenuType.Appetizer),
타파스(Price(5_500), MenuType.Appetizer),
시저샐러드(Price(8_000), MenuType.Appetizer),
티본스테이크(Price(55_000), MenuType.Main),
바비큐립(Price(54_000), MenuType.Main),
해산물파스타(Price(35_000), MenuType.Main),
크리스마스파스타(Price(25_000), MenuType.Main),
초코케이크(Price(15_000), MenuType.Dessert),
아이스크림(Price(5_000), MenuType.Dessert),
제로콜라(Price(3_000), MenuType.Drink),
레드와인(Price(60_000), MenuType.Drink),
샴페인(Price(25_000), MenuType.Drink),
;

}

Choose a reason for hiding this comment

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

버디가 메뉴를 sealed 클래스로 구현했던 것 같습니다! 참고하시면 좋을 것 같습니다!
https://github.com/BDD-CLUB/kotlin-study/pull/15/files#diff-3b5a6a13c08ef3879d046a5270b468cd23fcabfb622a6a4bca5da265bde58b4a

enum class MenuType {
Appetizer,
Main,
Dessert,
Drink,
;
}

data class MenuWithCount(val menu: Menu, val count: Int) {
val benefitAmount = menu.price * count

override fun toString() = "${this.menu.name} ${this.count}개"
}
27 changes: 27 additions & 0 deletions kotlin-christmas/src/main/kotlin/christmas/model/Price.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package christmas.model

import christmas.validation.requireChristmas
import java.text.DecimalFormat

@JvmInline
value class Price(val value: Int) {
init {

Choose a reason for hiding this comment

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

금액 도메인의 연산을 따로 만드셨군요!
민감한 도메인인만큼 이렇게 관리하는 것도 안정성이 좋을 것 같다는 생각이 드네요.
고민에 적어주신 내용처럼 막상보니 operator구현을 어디까지 해야할지 난감하네요 😅 제 생각엔 구현하신 것처럼 당장 필요한, 자주 사용하는 것만 구현하면 된다고 생각합니다!

requireChristmas(value >= 0)
}

operator fun times(other: Int) = Price(this.value * other)

operator fun plus(other: Price) = Price(this.value + other.value)

operator fun minus(other: Price) = Price(this.value - other.value)

operator fun compareTo(other: Price) = this.value.compareTo(other.value)

operator fun compareTo(other: Int) = this.value.compareTo(other)

fun toMinusString() = "${DecimalFormat("#,###").format(-value)}원"

override fun toString() = "${DecimalFormat("#,###").format(value)}원"
}

fun Int.toPrice() = Price(this)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package christmas.model

import christmas.badge.Badge
import christmas.event.EventType

class UserBenefitInfo(
val benefitList: List<EventType>,
) {
val giveaway: EventType.Giveaway? = benefitList
.filterIsInstance<EventType.Giveaway>().firstOrNull()

val totalBenefitAmount: Price = benefitList.sumOf {
when (it) {
is EventType.Giveaway -> it.menuWithCount.menu.price.value * it.menuWithCount.count
is EventType.Discount -> it.price.value
}
}.toPrice()

val totalDiscountAmount: Price = benefitList
.filterIsInstance<EventType.Discount>()
.sumOf { it.price.value }
.toPrice()

val eventBadge = Badge.of(totalBenefitAmount)
}
38 changes: 38 additions & 0 deletions kotlin-christmas/src/main/kotlin/christmas/model/UserOrderInfo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package christmas.model

import christmas.menu.MenuType
import christmas.menu.MenuWithCount
import christmas.validation.requireChristmas
import java.time.LocalDate

private const val MAX_AVAILABLE_MENU_COUNT = 20

class UserOrderInfo(
val userOrderMenu: UserOrderMenu,
estimatedVisitDay: VisitDay,
) {
val estimatedVisitDate: LocalDate = LocalDate.of(2023, 12, estimatedVisitDay.day)
val totalPrice: Price = userOrderMenu.getTotalPrice()

fun getSumPriceOfMenu(menuType: MenuType) =
this.userOrderMenu.menus.sumOf { if (it.menu.menuType == menuType) it.count else 0 }

@JvmInline
value class VisitDay(val day: Int) {
init {
requireChristmas(day in 1..31) { "유효하지 않은 날짜입니다. 다시 입력해 주세요." }
}
}

@JvmInline
value class UserOrderMenu(val menus: List<MenuWithCount>) {
init {
requireChristmas(menus.sumOf { it.count } <= MAX_AVAILABLE_MENU_COUNT) { "totalMenuCount는 ${MAX_AVAILABLE_MENU_COUNT}개를 넘을 수 없습니다." }
requireChristmas((menus.all { it.menu.menuType == MenuType.Drink }).not()) { "음료만 주문할 수 없습니다. 주문 메뉴: $menus" }
}

fun getTotalPrice() = this.menus.sumOf { it.menu.price.value * it.count }.toPrice()

override fun toString() = menus.joinToString("\n")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package christmas.service

import christmas.event.EventPolicy
import christmas.event.EventType
import christmas.model.UserBenefitInfo
import christmas.model.UserOrderInfo

object ChristmasService {

private val eventPolicyList = EventPolicy.entries

fun getBenefitInfo(userOrderInfo: UserOrderInfo) = UserBenefitInfo(getBenefitList(userOrderInfo))

private fun getBenefitList(userOrderInfo: UserOrderInfo) =
mutableListOf<EventType>().apply {
for (eventPolicy in eventPolicyList) {
if (eventPolicy.isSupported(userOrderInfo)) {
this.add(eventPolicy.getBenefit(userOrderInfo))
}
}
}.toList()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package christmas.validation

const val ERROR_PREFIX = "[ERROR] "

fun requireChristmas(value: Boolean) {
requireChristmas(value) { "Failed requirement." }
}

inline fun requireChristmas(value: Boolean, lazyMessage: () -> Any) {

Choose a reason for hiding this comment

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

제가 고민하던 부분이었는데 이렇게 하면 init에서도 exception을 제어할 수 있네요🤨

if (!value) {
throw ChristmasException(lazyMessage().toString())
}
}

class ChristmasException(message: String) : IllegalArgumentException() {
override val message = ERROR_PREFIX + message
}
Loading