Skip to content

Commit 7c9ae2d

Browse files
authored
Merge pull request #5 from lee-ji-hoon/ezhoon
[1장_이지훈] Pull Request
2 parents 52339fb + e02f567 commit 7c9ae2d

File tree

12 files changed

+321
-24
lines changed

12 files changed

+321
-24
lines changed

src/main/kotlin/Main.kt

Lines changed: 0 additions & 7 deletions
This file was deleted.

src/main/kotlin/ezhoon/InitClass.kt

Lines changed: 0 additions & 4 deletions
This file was deleted.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package ezhoon.chapter01
2+
3+
/**
4+
* 관람객 클래스
5+
*
6+
* @property bag : 소지품을 보관하기 위한 가방 클래스
7+
*/
8+
data class Audience(
9+
private var bag: Bag,
10+
) {
11+
12+
/**
13+
* TicketSeller.kt sellToLegacy(audience: Audience) 에서 캡슐화된 코드
14+
* 이전에는 TicketSeller에서 Audience의 Bag에 접근했기 때문에 Audience은 수동적이며 의존도가 높았다.
15+
* Bag에 관련된 내용을 Audience로 빼면서 TicketSeller는 Bag을 알 필요가 없어져 캡슐화 + 의존도가 낮아졌다.
16+
*
17+
* @param ticket 티켓
18+
* @return 관람객이 현장에서 지불할 금액
19+
*/
20+
/*
21+
fun buyLegacy(ticket: Ticket): Long {
22+
bag.ticket = ticket
23+
24+
return if (bag.hasInvitation.not()) {
25+
bag.minusAmount(ticket.fee)
26+
ticket.fee
27+
} else {
28+
0
29+
}
30+
}
31+
*/
32+
33+
/**
34+
* Bag에게도 능동적으로 일을 할 책임을 주기 위해서 리팩토링
35+
* @param ticket 티켓
36+
* @return 관람객이 현장에서 지불할 금액
37+
*/
38+
fun buy(ticket: Ticket): Long {
39+
return bag.hold(ticket)
40+
}
41+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package ezhoon.chapter01
2+
3+
/**
4+
* 관람객이 소지품을 보관할 클래스
5+
*
6+
* @property amount : 가격
7+
* @property invitation : 초대장
8+
* @property ticket : 티켓
9+
*/
10+
data class Bag(
11+
private var amount: Long,
12+
private var invitation: Invitation? = null,
13+
private var ticket: Ticket? = null,
14+
) {
15+
private val hasInvitation: Boolean
16+
get() = invitation != null
17+
val hasTicket: Boolean
18+
get() = ticket != null
19+
20+
fun hold(ticket: Ticket): Long {
21+
this.ticket = ticket
22+
23+
return if (hasInvitation.not()) {
24+
minusAmount(ticket.fee)
25+
ticket.fee
26+
} else {
27+
0
28+
}
29+
}
30+
31+
fun getAmount(): Long {
32+
return amount
33+
}
34+
35+
private fun minusAmount(amount: Long) {
36+
this.amount -= amount
37+
}
38+
39+
private fun plusAmount(amount: Long) {
40+
this.amount += amount
41+
}
42+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package ezhoon.chapter01
2+
3+
import java.time.LocalDateTime
4+
5+
/**
6+
* 초대장
7+
*
8+
* @property whenTime : 공연 시간
9+
*/
10+
data class Invitation(
11+
val whenTime: LocalDateTime,
12+
)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
2+
### 설계는 균형의 예술이다.
3+
4+
- 훌륭한 설계는 트레이드오프의 결과물이다.
5+
- 각 클래스가 직접 책임을 지게 만들 수 있지만 그렇게 하면 결합도가 높아지는 아이러니한 상황이 생긴다.
6+
- 이런 상황에서 어떤 트레이드오프를 기준으로 설계를 할지는 팀 혹은 개인의 관점에 따라 다르다.
7+
- 모든 사람을 만족시키는 이상적인 코딩따윈 존재하지 않는다.
8+
9+
10+
### 실생활에서는 자율적인 존재가 아닐지라도 코드에서는 스스로 책임지는 경우가 생긴다.
11+
12+
- 소극장(Theater)와 가방(Bag) 은 실세계에서 자율적인 존재가 아니다.
13+
- 하지만 우리는 이들을 관람객(Audience)이나 판매원(TicketSeller)과 같이 생물처럼 다뤘다.
14+
- 레베카 워프스브록은 이처럼 능동적이고 자율적은 존재로 소프트웨어 객체를 설계하는 원칙을 의인화라고 한다.
15+
16+
### 설계란 코드를 배치하는 것이다.
17+
18+
> 예제에서 내가 1차로 짰던 코드를 생각하면 그때도 테스트 코드는 똑같이 동작한다.
19+
> 하지만 코드를 배치하는 방법은 완전히 달랐다.
20+
21+
- 첫 번째에서는 데이터와 프로세스를 나눠서 별도의 클래스에 배치했다.
22+
- 두 번째에서는 필요한 데이터를 보유한 클래스 안에 프로세스를 함께 배치했다.
23+
- 즉 두 프로그램은 서로 다른 설계를 갖고 배치가 됐었다.
24+
25+
### 좋은 설계란?
26+
27+
> 요구하는 기능을 온전히 수행하면서 내일의 변경을 매끄럽게 수용할 수 있는 설계이다.
28+
29+
- 요구사항이 항상 변경되기 때문이다.
30+
- 필연적으로 코드의 수정도 생길 것이고, 버그도 같이 생기게 될 것이다.
31+
- 개발을 시작하자마자 모든 요구사항이 완벽하게 잡히는 것은 불가능하다.
32+
- 심지어는 배포 이후에 이전 구현으로 롤백되는 경우도 존재했다.
33+
34+
### 객체지향 설계
35+
36+
> 의존성을 효율적으로 통제할 수 있는 다양한 방법을 제공함으로써 요구사항 변경에 수월하게 대응이 가능하다.
37+
38+
- 세상에 존재하는 모든 자율적인 존재처럼 객체 역시 자신의 데이터를 스스로 책임진다.
39+
- 객체들 사이의 상호작용은 객체 사이에 주고 받는 메시지로 표현된다.
40+
- 협력하는 객체 사이의 의존성을 적절히 관리하는 것이 훌륭한 객체지향 설계이다.
41+
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package ezhoon.chapter01
2+
3+
class Theater(
4+
private val ticketSeller: TicketSeller
5+
) {
6+
7+
/**
8+
* TODO 리팩토링 대상이다. 역할과 의존성이 현재 이상하다. -> 소극장(Theater)와 너무 많은 것들이 결합돼있다.
9+
* - 소극장(Theater)가 관람객(Audience)의 가방(Audience.Bag)을 직접 확인
10+
* - 만약 티켓(Audience.Bag.Ticket)이 없다면 직접 현금(Audience.Bag.Amount)을 꺼내서 지불한다.
11+
* - 소극장(Theater)가 판매원(TicketSeller)에게 받은 요금(Ticket.Fee)만큼 Amount를 증가시킨다.
12+
* 관람객과 판매원이 소극장의 통제를 너무 과하게 받는 모습으로 보인다.
13+
* 현실에서는 관람객이 직접 자신의 가방에서 초대장을 꺼내 판매원에게 건내고, 판매원은 매표소에 있는 티켓을 직접 꺼내서 관람객에게 건내고 돈을 받아 매표소에 보관한다.
14+
* @param audience 관람객
15+
*/
16+
/*
17+
fun enterLegacy(audience: Audience) {
18+
val ticket = ticketSeller.ticketOffice.getTicket()
19+
20+
// 티켓이 없다면 방문객의 지갑에서 티켓 요금만큼 사용
21+
if (audience.bag.hasInvitation.not()) {
22+
audience.bag.minusAmount(ticket.fee)
23+
ticketSeller.ticketOffice.plusAmount(ticket.fee)
24+
}
25+
audience.bag.ticket = ticket
26+
}
27+
*/
28+
29+
/**
30+
* 소극장(Theater)는 판매자(TicketSeller) 관람객(Audience)가 무슨 일을 하는지 내부적으로 알 필요가 없기에 요청만 하면 된다.
31+
* @param audience 관람객
32+
*/
33+
fun enter(audience: Audience) {
34+
ticketSeller.sellTo(audience)
35+
}
36+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package ezhoon.chapter01
2+
3+
/**
4+
* 티켓 클래스
5+
*
6+
* @property fee : 가격
7+
*/
8+
data class Ticket(
9+
val fee: Long,
10+
)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package ezhoon.chapter01
2+
3+
/**
4+
* 티켓 판매
5+
*
6+
* @property amount : 가격
7+
* @property tickets : 티켓들
8+
*/
9+
data class TicketOffice(
10+
private var amount: Long,
11+
private var tickets: MutableList<Ticket> = mutableListOf()
12+
) {
13+
14+
/**
15+
* TODO TickOffice가 본인의 역할을 하는 것은 좋지만 audience와 결합이 됐다.
16+
* TickOffice가 본인의 책임을 다 할 수 있게 변경이 됐다.
17+
* 하지만 다른 클래스와 결합이 생기는 등 트레이드오프가 어느정도 생겼다.
18+
* @param audience 관람객
19+
*/
20+
fun sellTicketTo(audience: Audience) {
21+
plusAmount(audience.buy(getTicket()))
22+
}
23+
24+
private fun getTicket(): Ticket {
25+
return tickets.removeAt(0)
26+
}
27+
28+
private fun minusAmount(amount: Long) {
29+
this.amount -= amount
30+
}
31+
32+
private fun plusAmount(amount: Long) {
33+
this.amount += amount
34+
}
35+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package ezhoon.chapter01
2+
3+
data class TicketSeller(
4+
private val ticketOffice: TicketOffice
5+
) {
6+
/**
7+
* Theater.kt fun enterLegacy(audience: Audience) 에서 캡슐화된 코드
8+
* TicketOffice가 private객체가 되면서 세부적인 사항이 감쳐졌다 -> 캡슐화 + 결합도 낮춤
9+
* 하지만 지금도 관람객(Audience)의 Bag에 접근을 하고 있어서 의존도가 너무 높다! 리팩토링 하자!
10+
* @param audience 관람객
11+
*/
12+
/*
13+
fun sellToLegacy(audience: Audience) {
14+
val ticket = ticketOffice.getTicket()
15+
16+
// 티켓이 없다면 방문객의 지갑에서 티켓 요금만큼 사용
17+
if (audience.bag.hasInvitation.not()) {
18+
audience.bag.minusAmount(ticket.fee)
19+
ticketOffice.plusAmount(ticket.fee)
20+
}
21+
audience.bag.ticket = ticket
22+
}
23+
*/
24+
25+
/**
26+
* 관람객(Audience)의 Bag관련된 내용은 Audience가 처리하게 변경이 됐다.
27+
* 이렇게 함으로써 Bag의 내부 구현이 변경이 되더라도 TicketSeller는 아무런 영향을 받지 않는다.
28+
* @param audience 관람객
29+
*/
30+
/*
31+
fun sellToLegacy(audience: Audience) {
32+
val ticket = ticketOffice.getTicket()
33+
val amount = audience.buy(ticket)
34+
ticketOffice.plusAmount(amount)
35+
}
36+
*/
37+
38+
/**
39+
* TicketOffice 에게도 자율적으로 책임을 질 수 있게 변경
40+
* @param audience 관람객
41+
*/
42+
fun sellTo(audience: Audience) {
43+
ticketOffice.sellTicketTo(audience)
44+
}
45+
}

src/test/kotlin/MainKtTest.kt

Lines changed: 0 additions & 13 deletions
This file was deleted.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package ezhoon.chapter01
2+
3+
import ezhoon.chapter01.*
4+
import org.junit.jupiter.api.BeforeEach
5+
import org.junit.jupiter.api.DisplayName
6+
import org.junit.jupiter.api.Test
7+
import java.time.LocalDateTime
8+
import kotlin.test.assertEquals
9+
import kotlin.test.assertTrue
10+
11+
@DisplayName("관람객 클래스 테스트")
12+
class AudienceTest {
13+
14+
private lateinit var ticketSeller: TicketSeller
15+
private lateinit var ticketOffice: TicketOffice
16+
private lateinit var theater: Theater
17+
18+
@BeforeEach
19+
fun initTheater() {
20+
ticketOffice = TicketOffice(10, tickets)
21+
ticketSeller = TicketSeller(ticketOffice)
22+
theater = Theater(ticketSeller)
23+
}
24+
25+
@Test
26+
@DisplayName("티켓/초대권이 없는 경우 티켓 금액만큼 현재 갖고 있는 금액에서 빼고 티켓을 갖고 있어야 한다.")
27+
fun noHaveTicketAudience() {
28+
// given
29+
val bag = Bag(audienceInitAmount)
30+
val audience = Audience(bag)
31+
// when
32+
theater.enter(audience)
33+
// then
34+
assertEquals(bag.getAmount(), audienceInitAmount - ticket.fee)
35+
assertTrue(bag.hasTicket)
36+
}
37+
38+
@Test
39+
@DisplayName("초대권을 갖고 있는 경우 금액은 그대로고 ticket이 생성돼야 한다.")
40+
fun haveTicketAudience() {
41+
// given
42+
val invitation = Invitation(LocalDateTime.now())
43+
val bag = Bag(audienceInitAmount, invitation)
44+
val audience = Audience(bag)
45+
// when
46+
theater.enter(audience)
47+
// then
48+
assertEquals(bag.getAmount(), audienceInitAmount)
49+
assertTrue(bag.hasTicket)
50+
}
51+
52+
companion object {
53+
private val ticket = Ticket(10_000L)
54+
val tickets = generateSequence(ticket) { it }
55+
.take(10)
56+
.toMutableList()
57+
private const val audienceInitAmount = 50_000L
58+
}
59+
}

0 commit comments

Comments
 (0)