Skip to content

[1장_이지훈] Pull Request #5

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

Merged
merged 10 commits into from
Mar 19, 2023
7 changes: 0 additions & 7 deletions src/main/kotlin/Main.kt

This file was deleted.

4 changes: 0 additions & 4 deletions src/main/kotlin/ezhoon/InitClass.kt

This file was deleted.

41 changes: 41 additions & 0 deletions src/main/kotlin/ezhoon/chapter01/Audience.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package ezhoon.chapter01

/**
* 관람객 클래스
*
* @property bag : 소지품을 보관하기 위한 가방 클래스
*/
data class Audience(
private var bag: Bag,
) {

/**
* TicketSeller.kt sellToLegacy(audience: Audience) 에서 캡슐화된 코드
* 이전에는 TicketSeller에서 Audience의 Bag에 접근했기 때문에 Audience은 수동적이며 의존도가 높았다.
* Bag에 관련된 내용을 Audience로 빼면서 TicketSeller는 Bag을 알 필요가 없어져 캡슐화 + 의존도가 낮아졌다.
*
* @param ticket 티켓
* @return 관람객이 현장에서 지불할 금액
*/
/*
fun buyLegacy(ticket: Ticket): Long {
bag.ticket = ticket

return if (bag.hasInvitation.not()) {
bag.minusAmount(ticket.fee)
ticket.fee
} else {
0
}
}
*/

/**
* Bag에게도 능동적으로 일을 할 책임을 주기 위해서 리팩토링
* @param ticket 티켓
* @return 관람객이 현장에서 지불할 금액
*/
fun buy(ticket: Ticket): Long {
return bag.hold(ticket)
}
}
42 changes: 42 additions & 0 deletions src/main/kotlin/ezhoon/chapter01/Bag.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package ezhoon.chapter01

/**
* 관람객이 소지품을 보관할 클래스
*
* @property amount : 가격
* @property invitation : 초대장
* @property ticket : 티켓
*/
data class Bag(
private var amount: Long,
private var invitation: Invitation? = null,
private var ticket: Ticket? = null,
) {
private val hasInvitation: Boolean
get() = invitation != null
val hasTicket: Boolean
get() = ticket != null

fun hold(ticket: Ticket): Long {
this.ticket = ticket

return if (hasInvitation.not()) {
minusAmount(ticket.fee)
ticket.fee
} else {
0
}
}

fun getAmount(): Long {
return amount
}

private fun minusAmount(amount: Long) {
this.amount -= amount
}

private fun plusAmount(amount: Long) {
this.amount += amount
}
}
12 changes: 12 additions & 0 deletions src/main/kotlin/ezhoon/chapter01/Invitation.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ezhoon.chapter01

import java.time.LocalDateTime

/**
* 초대장
*
* @property whenTime : 공연 시간
*/
data class Invitation(
val whenTime: LocalDateTime,
)
41 changes: 41 additions & 0 deletions src/main/kotlin/ezhoon/chapter01/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@

### 설계는 균형의 예술이다.

- 훌륭한 설계는 트레이드오프의 결과물이다.
- 각 클래스가 직접 책임을 지게 만들 수 있지만 그렇게 하면 결합도가 높아지는 아이러니한 상황이 생긴다.
- 이런 상황에서 어떤 트레이드오프를 기준으로 설계를 할지는 팀 혹은 개인의 관점에 따라 다르다.
- 모든 사람을 만족시키는 이상적인 코딩따윈 존재하지 않는다.


### 실생활에서는 자율적인 존재가 아닐지라도 코드에서는 스스로 책임지는 경우가 생긴다.

- 소극장(Theater)와 가방(Bag) 은 실세계에서 자율적인 존재가 아니다.
- 하지만 우리는 이들을 관람객(Audience)이나 판매원(TicketSeller)과 같이 생물처럼 다뤘다.
- 레베카 워프스브록은 이처럼 능동적이고 자율적은 존재로 소프트웨어 객체를 설계하는 원칙을 의인화라고 한다.

### 설계란 코드를 배치하는 것이다.

> 예제에서 내가 1차로 짰던 코드를 생각하면 그때도 테스트 코드는 똑같이 동작한다.
> 하지만 코드를 배치하는 방법은 완전히 달랐다.

- 첫 번째에서는 데이터와 프로세스를 나눠서 별도의 클래스에 배치했다.
- 두 번째에서는 필요한 데이터를 보유한 클래스 안에 프로세스를 함께 배치했다.
- 즉 두 프로그램은 서로 다른 설계를 갖고 배치가 됐었다.

### 좋은 설계란?

> 요구하는 기능을 온전히 수행하면서 내일의 변경을 매끄럽게 수용할 수 있는 설계이다.

- 요구사항이 항상 변경되기 때문이다.
- 필연적으로 코드의 수정도 생길 것이고, 버그도 같이 생기게 될 것이다.
- 개발을 시작하자마자 모든 요구사항이 완벽하게 잡히는 것은 불가능하다.
- 심지어는 배포 이후에 이전 구현으로 롤백되는 경우도 존재했다.

### 객체지향 설계

> 의존성을 효율적으로 통제할 수 있는 다양한 방법을 제공함으로써 요구사항 변경에 수월하게 대응이 가능하다.

- 세상에 존재하는 모든 자율적인 존재처럼 객체 역시 자신의 데이터를 스스로 책임진다.
- 객체들 사이의 상호작용은 객체 사이에 주고 받는 메시지로 표현된다.
- 협력하는 객체 사이의 의존성을 적절히 관리하는 것이 훌륭한 객체지향 설계이다.

36 changes: 36 additions & 0 deletions src/main/kotlin/ezhoon/chapter01/Theater.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package ezhoon.chapter01

class Theater(
private val ticketSeller: TicketSeller
) {

/**
* TODO 리팩토링 대상이다. 역할과 의존성이 현재 이상하다. -> 소극장(Theater)와 너무 많은 것들이 결합돼있다.
* - 소극장(Theater)가 관람객(Audience)의 가방(Audience.Bag)을 직접 확인
* - 만약 티켓(Audience.Bag.Ticket)이 없다면 직접 현금(Audience.Bag.Amount)을 꺼내서 지불한다.
* - 소극장(Theater)가 판매원(TicketSeller)에게 받은 요금(Ticket.Fee)만큼 Amount를 증가시킨다.
* 관람객과 판매원이 소극장의 통제를 너무 과하게 받는 모습으로 보인다.
* 현실에서는 관람객이 직접 자신의 가방에서 초대장을 꺼내 판매원에게 건내고, 판매원은 매표소에 있는 티켓을 직접 꺼내서 관람객에게 건내고 돈을 받아 매표소에 보관한다.
* @param audience 관람객
*/
/*
fun enterLegacy(audience: Audience) {
val ticket = ticketSeller.ticketOffice.getTicket()

// 티켓이 없다면 방문객의 지갑에서 티켓 요금만큼 사용
if (audience.bag.hasInvitation.not()) {
audience.bag.minusAmount(ticket.fee)
ticketSeller.ticketOffice.plusAmount(ticket.fee)
}
audience.bag.ticket = ticket
}
*/

/**
* 소극장(Theater)는 판매자(TicketSeller) 관람객(Audience)가 무슨 일을 하는지 내부적으로 알 필요가 없기에 요청만 하면 된다.
* @param audience 관람객
*/
fun enter(audience: Audience) {
ticketSeller.sellTo(audience)
}
}
10 changes: 10 additions & 0 deletions src/main/kotlin/ezhoon/chapter01/Ticket.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package ezhoon.chapter01

/**
* 티켓 클래스
*
* @property fee : 가격
*/
data class Ticket(
val fee: Long,
)
35 changes: 35 additions & 0 deletions src/main/kotlin/ezhoon/chapter01/TicketOffice.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package ezhoon.chapter01

/**
* 티켓 판매
*
* @property amount : 가격
* @property tickets : 티켓들
*/
data class TicketOffice(
private var amount: Long,
private var tickets: MutableList<Ticket> = mutableListOf()
) {

/**
* TODO TickOffice가 본인의 역할을 하는 것은 좋지만 audience와 결합이 됐다.
* TickOffice가 본인의 책임을 다 할 수 있게 변경이 됐다.
* 하지만 다른 클래스와 결합이 생기는 등 트레이드오프가 어느정도 생겼다.
* @param audience 관람객
*/
fun sellTicketTo(audience: Audience) {
plusAmount(audience.buy(getTicket()))
}

private fun getTicket(): Ticket {
return tickets.removeAt(0)
}

private fun minusAmount(amount: Long) {
this.amount -= amount
}

private fun plusAmount(amount: Long) {
this.amount += amount
}
}
45 changes: 45 additions & 0 deletions src/main/kotlin/ezhoon/chapter01/TicketSeller.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package ezhoon.chapter01

data class TicketSeller(
private val ticketOffice: TicketOffice
) {
/**
* Theater.kt fun enterLegacy(audience: Audience) 에서 캡슐화된 코드
* TicketOffice가 private객체가 되면서 세부적인 사항이 감쳐졌다 -> 캡슐화 + 결합도 낮춤
* 하지만 지금도 관람객(Audience)의 Bag에 접근을 하고 있어서 의존도가 너무 높다! 리팩토링 하자!
* @param audience 관람객
*/
/*
fun sellToLegacy(audience: Audience) {
val ticket = ticketOffice.getTicket()

// 티켓이 없다면 방문객의 지갑에서 티켓 요금만큼 사용
if (audience.bag.hasInvitation.not()) {
audience.bag.minusAmount(ticket.fee)
ticketOffice.plusAmount(ticket.fee)
}
audience.bag.ticket = ticket
}
*/

/**
* 관람객(Audience)의 Bag관련된 내용은 Audience가 처리하게 변경이 됐다.
* 이렇게 함으로써 Bag의 내부 구현이 변경이 되더라도 TicketSeller는 아무런 영향을 받지 않는다.
* @param audience 관람객
*/
/*
fun sellToLegacy(audience: Audience) {
val ticket = ticketOffice.getTicket()
val amount = audience.buy(ticket)
ticketOffice.plusAmount(amount)
}
*/

/**
* TicketOffice 에게도 자율적으로 책임을 질 수 있게 변경
* @param audience 관람객
*/
fun sellTo(audience: Audience) {
ticketOffice.sellTicketTo(audience)
}
}
13 changes: 0 additions & 13 deletions src/test/kotlin/MainKtTest.kt

This file was deleted.

59 changes: 59 additions & 0 deletions src/test/kotlin/ezhoon/chapter01/AudienceTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package ezhoon.chapter01

import ezhoon.chapter01.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import java.time.LocalDateTime
import kotlin.test.assertEquals
import kotlin.test.assertTrue

@DisplayName("관람객 클래스 테스트")
class AudienceTest {

private lateinit var ticketSeller: TicketSeller
private lateinit var ticketOffice: TicketOffice
private lateinit var theater: Theater

@BeforeEach
fun initTheater() {
ticketOffice = TicketOffice(10, tickets)
ticketSeller = TicketSeller(ticketOffice)
theater = Theater(ticketSeller)
}

@Test
@DisplayName("티켓/초대권이 없는 경우 티켓 금액만큼 현재 갖고 있는 금액에서 빼고 티켓을 갖고 있어야 한다.")
fun noHaveTicketAudience() {
// given
val bag = Bag(audienceInitAmount)
val audience = Audience(bag)
// when
theater.enter(audience)
// then
assertEquals(bag.getAmount(), audienceInitAmount - ticket.fee)
assertTrue(bag.hasTicket)
}

@Test
@DisplayName("초대권을 갖고 있는 경우 금액은 그대로고 ticket이 생성돼야 한다.")
fun haveTicketAudience() {
// given
val invitation = Invitation(LocalDateTime.now())
val bag = Bag(audienceInitAmount, invitation)
val audience = Audience(bag)
// when
theater.enter(audience)
// then
assertEquals(bag.getAmount(), audienceInitAmount)
assertTrue(bag.hasTicket)
}

companion object {
private val ticket = Ticket(10_000L)
val tickets = generateSequence(ticket) { it }
.take(10)
.toMutableList()
private const val audienceInitAmount = 50_000L
}
}