DevLog ๐ถ
[Intellij] ์์ ํ ๋ฆฌํฉํฐ๋ง ์งํํ๊ธฐ - Intellij๋ฅผ ํ์ฉํ ์ ์ง์ ๋ฆฌํฉํฐ๋ง ๋ณธ๋ฌธ
[Intellij] ์์ ํ ๋ฆฌํฉํฐ๋ง ์งํํ๊ธฐ - Intellij๋ฅผ ํ์ฉํ ์ ์ง์ ๋ฆฌํฉํฐ๋ง
dolmeng2 2024. 2. 25. 16:04๐ฑ ๋ค์ด๊ฐ๊ธฐ ์
๋๋ ํ์์ ๋ง์ฐ์ค๋ ์ฌ์ฉํ์ง ์์ง๋ง, ๋งฅ๋ถ ํฐ์นํจ๋๋ฅผ ์ ๋ง ๋ง์ด ์ฌ์ฉํ๋ ํธ์ด์์ด์ ๊ฐ๋ฐํ ๋ ๋จ์ถํค๋ฅผ๋ง์ด ์ฌ์ฉํ์ง ์๋ ํธ์ด์๋ค. ๊ทธ๋ฌ๋ค ๋ณด๋ ์๋์ผ๋ก ๊ฐ๋ฐํ ๋ ๋ฏธ๋ฌํ๊ฒ ์๋ ์ฐจ์ด๊ฐ ๋ฌ์๋๋ฐ, ์ด๋ฒ์ ๋จ์ถํค๋ ๊ณต๋ถํ ๊ฒธ, ๋ฆฌํฉํฐ๋ง ์ ์ด๋ป๊ฒ ํ๋ฉด Intellij ๋ฅผ ์ต๋ํ ํ์ฉํ ์ ์๋์ง ์คํฐ๋ ํ๋ ์๊ฐ์ ๊ฐ์ก๋ค. (๊ฐ๋๋๊ฐ๋ฐ์๋ ๋๋ถ์ ๋ง์ด ๋ฐฐ์ธ ์ ์๋ ์๊ฐ์ด์๋ค ใ _ใ )
์ด๋ฒ ํฌ์คํ ์์ ์ฌ์ฉ๋ ์ํ ์ฝ๋๋ ๋ฐฑ๋ช ์ ๋์ ๊นํ๋ธ๋ฅผ ๊ฐ๋ฉด ํ์ธํ ์ ์๋๋ฐ, ๋๋ ์ฌ๋ด์์ ์ฝํ๋ฆฐ์ ์ฌ์ฉํ๊ณ ์๋ค ๋ณด๋๊น ํด๋น ์ฝ๋๋ฅผ ์ฝํ๋ฆฐ + Kotest๋ก ๋ณํํ์ฌ ์ฐ์ต์ ํด๋ณด์๋ค.
์ด๋ค ์์ผ๋ก ๋ฆฌํฉํฐ๋ง์ ํ๋์ง๋ ์๋์ ๋ ํ์งํ ๋ฆฌ์ ์ปค๋ฐ๋ณ๋ก ๋ํ๋ด์๋ค.
main ๋ธ๋์น์ ๊ฒฝ์ฐ ๋ฆฌํฉํฐ๋ง ์ด์ ์ ์ฝ๋์ด๊ณ , refactoring ๋ธ๋์น๋ฅผ ๊ฐ๋ฉด ์ ์ง์ ๋ฆฌํฉํฐ๋ง ๊ณผ์ ์ ์ฐธ๊ณ ํ ์ ์์ผ๋ ํน์ ํ์ํ ๋ถ์ด ๊ณ์๋ค๋ฉด ์ฐธ๊ณ ํ๋ฉด ์ข์ ๊ฒ ๊ฐ๋ค.
๐ฑ ์๋ณธ ์ฝ๋์ ๋ฌธ์ ์
์ฐ์ , ๋ฆฌํฉํฐ๋ง์ด ๋๊ธฐ ์ด์ ์ ์ํ ์ฝ๋์ด๋ค.
class Expense(
var type: Type,
var amount: Int
) {
enum class Type {
DINNER,
BREAKFAST,
CAR_RENTAL
}
}
interface ReportPrinter {
fun print(text: String?)
}
class ExpenseReport {
private val expenses: MutableList<Expense> = ArrayList()
private val date: String
get() = "9/12/2002"
fun printReport(printer: ReportPrinter) {
var total = 0
var mealExpenses = 0
printer.print("Expenses " + date + "\\n")
for (expense in expenses) {
if (expense.type == Expense.Type.BREAKFAST || expense.type == Expense.Type.DINNER) {
mealExpenses += expense.amount
}
var name = "TILT"
when (expense.type) {
Expense.Type.DINNER -> name = "Dinner"
Expense.Type.BREAKFAST -> name = "Breakfast"
Expense.Type.CAR_RENTAL -> name = "Car Rental"
}
printer.print(
String.format(
"%s\\t%s\\t$%.02f\\n",
if (expense.type == Expense.Type.DINNER
&& expense.amount > 5000
|| expense.type === Expense.Type.BREAKFAST
&& expense.amount > 1000) "X" else " ",
name, expense.amount / 100.0
)
)
total += expense.amount
}
printer.print(String.format("\\nMeal expenses $%.02f", mealExpenses / 100.0))
printer.print(String.format("\\nTotal $%.02f", total / 100.0))
}
fun addExpense(expense: Expense) {
expenses.add(expense)
}
}
์์ผ๋ก ์์ ์ฝ๋๊ฐ ์ฐ๋ฆฌ๊ฐ ์ํ๋ ์๋๋ฆฌ์ค๋๋ก ์ ๋์ํ๋์ง ๊ฒ์ฆํ ์ ์๋๋ก, ์๋์ ํ ์คํธ ์ฝ๋๋ฅผ ์ฌ์ฉํ ๊ฒ์ด๋ค.
๋ชจ๋ ๋ฆฌํฉํฐ๋ง์ ์์์, ‘์ฐ๋ฆฌ๊ฐ ์ฝ๋๋ฅผ ์์ ํ๋๋ผ๋ ์ ์์ ์ผ๋ก ์๋ํ๋ ๊ฒ์ ๋ณด์ฅ๋ฐ์ ์ ์๋๊ฐ?’๋ก๋ถํฐ ์์ํ๋ ๊ฒ ๊ฐ๋ค. ์ค๋ฌด์์๋ ํ ์คํธ ์ฝ๋๊ฐ ์์ฑ๋์ด ์์ง ์๋ค๋ฉด ์ด๋ ต๊ฒ ์ง๋ง, ์ฌ๋งํ๋ฉด ๋ฆฌํฉํฐ๋ง์ ์งํํ ๋ ํ ์คํธ ์ฝ๋๋ฅผ ํจ๊ป ๋๋ฐํ๋ ๊ฒ์ด ์ข์ ๊ฒ ๊ฐ๋ค.
class MockReportPrinter : ReportPrinter {
private var printedText = ""
override fun print(text: String?) {
printedText += text
}
fun getText(): String {
return printedText
}
}
internal class ExpenseReportTest : StringSpec({
lateinit var report: ExpenseReport
lateinit var printer: MockReportPrinter
beforeTest {
report = ExpenseReport()
printer = MockReportPrinter()
}
"printEmpty" {
report.printReport(printer)
printer.getText() shouldBe """
Expenses 9/12/2002
Meal expenses $0.00
Total $0.00
""".trimIndent()
}
"printOneDinner" {
report.addExpense(Expense(Expense.Type.DINNER, 1678))
report.printReport(printer)
printer.getText() shouldBe """
Expenses 9/12/2002
Dinner $16.78
Meal expenses $16.78
Total $16.78
""".trimIndent()
}
"twoMeals" {
report.addExpense(Expense(Expense.Type.DINNER, 1000))
report.addExpense(Expense(Expense.Type.BREAKFAST, 500))
report.printReport(printer)
printer.getText() shouldBe """
Expenses 9/12/2002
Dinner $10.00
Breakfast $5.00
Meal expenses $15.00
Total $15.00
""".trimIndent()
}
"twoMealsAndCarRental" {
report.addExpense(Expense(Expense.Type.DINNER, 1000))
report.addExpense(Expense(Expense.Type.BREAKFAST, 500))
report.addExpense(Expense(Expense.Type.CAR_RENTAL, 50000))
report.printReport(printer)
printer.getText() shouldBe """
Expenses 9/12/2002
Dinner $10.00
Breakfast $5.00
Car Rental $500.00
Meal expenses $15.00
Total $515.00
""".trimIndent()
}
"overages" {
report.addExpense(Expense(Expense.Type.BREAKFAST, 1000))
report.addExpense(Expense(Expense.Type.BREAKFAST, 1001))
report.addExpense(Expense(Expense.Type.DINNER, 5000))
report.addExpense(Expense(Expense.Type.DINNER, 5001))
report.printReport(printer)
printer.getText() shouldBe """
Expenses 9/12/2002
Breakfast $10.00
X Breakfast $10.01
Dinner $50.00
X Dinner $50.01
Meal expenses $120.02
Total $120.02
""".trimIndent()
}
})
๊ธฐ์กด์ ์ฝ๋์ ๊ฒฝ์ฐ ๋ค์๊ณผ ๊ฐ์ ๋ฌธ์ ์ ์ด ์กด์ฌํ๋ค.
1. ExpenseReport ๊ฐ ํ๊ณ ์๋ ์ผ์ด ๋๋ฌด ๋ง์
2. ๋น์ฉ์ ๋ํ ๊ณ์ฐ์ ํ๋ ํจ์์ ์ถ๋ ฅ์ ๋ํ ํจ์๊ฐ ํฉ์ณ์ ธ ์์ด์ ๊ธฐ๋ฅ ์์ฒด์ ๊ฒฐํฉ๋๊ฐ ๋์
3. ๋น์ฉ์ ๋ํ ๋ณ์์ ์ค์ฝํ๊ฐ printReport() ํจ์์ ์ง์ญ ๋ณ์๋ก ์ ์ธ๋์ด ์์ด, ๊ฐ๋ฐ์๊ฐ ์ธ์งํ๊ธฐ๊ฐ ์ด๋ ค์.
4. ๋น์ฉ์ ๋ํ ํ์ ์ด ์ถ๊ฐ๋์์ ๋ ํ์ฅํ๊ธฐ๊ฐ ์ด๋ ค์
๊ทธ๋์ ์ฐ๋ฆฌ๋ ์์ ๋ฌธ์ ์ ์ ์ต๋ํ ์์ ํ ์ ์๋๋ก ์ ์ง์ ์ธ ๋ฆฌํฉํฐ๋ง์ ์งํํ ๊ฒ์ด๋ค.
๐ฑ Step1 - ๊ฐ์ฅ ๊ฐ๋จํ ๋ถ๋ถ์ ๋ํด์ ํจ์๋ก ๋ถ๋ฆฌํ๊ธฐ
fun printReport(printer: ReportPrinter) {
var total = 0
var mealExpenses = 0
printer.print("Expenses " + date + "\\n")
for (expense in expenses) {
if (expense.type == Expense.Type.BREAKFAST || expense.type == Expense.Type.DINNER) {
mealExpenses += expense.amount
}
var name = "TILT"
when (expense.type) {
Expense.Type.DINNER -> name = "Dinner"
Expense.Type.BREAKFAST -> name = "Breakfast"
Expense.Type.CAR_RENTAL -> name = "Car Rental"
}
printer.print(
String.format(
"%s\\t%s\\t$%.02f\\n",
if (expense.type == Expense.Type.DINNER
&& expense.amount > 5000
|| expense.type === Expense.Type.BREAKFAST
&& expense.amount > 1000) "X" else " ",
name, expense.amount / 100.0
)
)
total += expense.amount
}
printer.print(String.format("\\nMeal expenses $%.02f", mealExpenses / 100.0))
printer.print(String.format("\\nTotal $%.02f", total / 100.0))
}
๋จผ์ , ์ ํจ์์์ ๋ถ๋ฆฌํ ์ ์๋ ๋ถ๋ถ์ ์ต๋ํ ์ถ์ถํ์ฌ ํจ์๋ก ๋ง๋ค์ด๋ณด์.
์ฐ๋ฆฌ๋ ์์ ํจ์๋ฅผ ํฌ๊ฒ 3๊ฐ์ง ๋ถ๋ถ์ผ๋ก ๋ถ๋ฆฌํ ์ ์๋ค.
1. ํค๋ ์ ๋ณด ์ถ๋ ฅํ๊ธฐ
2. ๋น์ฉ์ ๊ณ์ฐํ๊ณ ์ค๊ฐ ๊ณ์ฐ ๊ฒฐ๊ณผ๋ฅผ ์ถ๋ ฅํ๊ธฐ
3. ์ต์ข ๋น์ฉ์ ์ถ๋ ฅํ๊ธฐ
๋น๊ต์ ๋ณต์กํ 2๋ฒ ๋ด์ฉ์ ์ฐ์ ๋ฐฐ์ ํ๊ณ , 1๋ฒ๊ณผ 3๋ฒ ๋ด์ฉ์ ์ถ์ถํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ด ํจ์๋ก ์ถ์ถํ์.
์ด๋, 1๋ฒ ๋ด์ฉ์์ total, mealExpenses ๋ ํค๋ ์ ๋ณด๋ฅผ ์ถ๋ ฅํ๊ธฐ ์ํด ์ฌ์ฉ๋๋ ์ ๋ณด๋ ์๋๊ธฐ ๋๋ฌธ์ ์ ์ธ๋ ๋ณ์๋ฅผ 2๋ฒ ๋ถ๋ถ์๊ฒ ๋ด๋ฆด ์ ์์ผ๋ฉฐ, 3๋ฒ ๋ถ๋ถ์ ๊ฒฝ์ฐ ํ๋ผ๋ฏธํฐ๋ฅผ ํตํด total, mealExpenses๋ฅผ ๋๊ฒจ์ฃผ๋ฉด ๋๊ธฐ ๋๋ฌธ์ ํฌ๊ฒ ๋ฌด๋ฆฌ๊ฐ ์๋ค.
command + option + m (extract method) ์ ํตํด ๊ฐ๊ฐ์ ํจ์๋ก ๋ถ๋ฆฌํ์.
๊ทธ๋ผ ์๋์ ๊ฐ์ด ๋ถ๋ฆฌ๊ฐ ๋์์ ํ ๋ฐ, ํ ์คํธ ์ฝ๋๋ฅผ ํตํด์ ์์ง ๊ธฐ๋ฅ์ด ์ ์๋ํ๋์ง ์ฒดํฌํ์.
fun printReport(printer: ReportPrinter) {
printHeader(printer)
var total = 0
var mealExpenses = 0
for (expense in expenses) {
if (expense.type == Expense.Type.BREAKFAST || expense.type == Expense.Type.DINNER) {
mealExpenses += expense.amount
}
var name = "TILT"
when (expense.type) {
Expense.Type.DINNER -> name = "Dinner"
Expense.Type.BREAKFAST -> name = "Breakfast"
Expense.Type.CAR_RENTAL -> name = "Car Rental"
}
printer.print(
String.format(
"%s\\t%s\\t$%.02f\\n",
if (expense.type == Expense.Type.DINNER
&& expense.amount > 5000
|| expense.type === Expense.Type.BREAKFAST
&& expense.amount > 1000) "X" else " ",
name, expense.amount / 100.0
)
)
total += expense.amount
}
printTotal(printer, mealExpenses, total)
}
private fun printTotal(printer: ReportPrinter, mealExpenses: Int, total: Int) {
printer.print(String.format("\\nMeal expenses $%.02f", mealExpenses / 100.0))
printer.print(String.format("\\nTotal $%.02f", total / 100.0))
}
private fun printHeader(printer: ReportPrinter) {
printer.print("Expenses " + date + "\\n")
}
ํ ์คํธ๊ฐ ์ ์์ ์ผ๋ก ํต๊ณผํ๋ค๋ฉด, ์์ฌํ๊ณ ๋ค์ ์คํ ์ผ๋ก ๋์ด๊ฐ๋ณด์.
๐ฑ Step 2 - ๋น์ฆ๋์ค ๋ก์ง๊ณผ ์ถ๋ ฅ์ ๋ถ๋ฆฌํ์
์ด ๋ค์์๋ 2๋ฒ ๋ถ๋ถ์ ๋ฟ์ค ์ฐจ๋ก์ด๋ค. ํด๋น ๋ถ๋ถ์ ์ถ๋ ฅ๊ณผ ๊ณ์ฐ์ด ํจ๊ป ํผํฉ๋์ด ์๊ธฐ ๋๋ฌธ์ ๋ถ๋ฆฌํ๊ธฐ๊ฐ ํ์ฌ๋ก์๋ ์ด๋ ค์ฐ๋, ๋ฐ๋ณต๋ฌธ์ 2๊ฐ๋ก ๋ถ๋ฆฌํ์ฌ ์ถ๋ ฅ๊ณผ ๊ณ์ฐ์ ๋ถ๋ฆฌํด๋ณด์.
๋ง์ฐฌ๊ฐ์ง๋ก ๋ถ๋ฆฌ ํ์๋ ํ ์คํธ ์ฝ๋๋ฅผ ์คํํด๋ณด์. ์ ํต๊ณผํ๋ ๋ชจ์ต์ ๋ณผ ์ ์์ ๊ฒ์ด๋ค.
์ฐธ๊ณ ๋ก, ์ฝ๋ ๋ธ๋ก์ ์ ํํ ๋ option + ๋ฐฉํฅํค ์ํค๋ฅผ ๋๋ฅด๊ฒ ๋๋ฉด ๋ธ๋ก๋ณ๋ก ํ ๋ฒ์ select๋ฅผ ํ ์ ์๋ค.
ํฐ์นํจ๋๋ ๋ง์ฐ์ค๋ฅผ ์ฐ์ง ์๊ณ ๋ ๋ธ๋ก์ ์ ํํ ์ ์๋ ์ข์ ํค์ด๋ค.
fun printReport(printer: ReportPrinter) {
printHeader(printer)
var total = 0
var mealExpenses = 0
for (expense in expenses) {
if (expense.type == Expense.Type.BREAKFAST || expense.type == Expense.Type.DINNER) {
mealExpenses += expense.amount
}
total += expense.amount
}
for (expense in expenses) {
var name = "TILT"
when (expense.type) {
Expense.Type.DINNER -> name = "Dinner"
Expense.Type.BREAKFAST -> name = "Breakfast"
Expense.Type.CAR_RENTAL -> name = "Car Rental"
}
printer.print(
String.format(
"%s\\t%s\\t$%.02f\\n",
if (expense.type == Expense.Type.DINNER
&& expense.amount > 5000
|| expense.type === Expense.Type.BREAKFAST
&& expense.amount > 1000) "X" else " ",
name, expense.amount / 100.0
)
)
}
printTotal(printer, mealExpenses, total)
}
๊ทธ๋ ๋ค๋ฉด, ์ด์ ๋น์ฉ์ ๊ณ์ฐํ๋ ๋ถ๋ถ์ ๋ํด์ ํจ์๋ก ๋ถ๋ฆฌํด๋ณด์.
์ด๋, Intellij์ ๋์์ ๋ฐ๋๋ค๋ฉด ๊ฝค๋ ์ด์ํ ํํ๋ก ๋ถ๋ฆฌ๋ฅผ ํด์ฃผ๋ ๋ชจ์ต์ ๋ณผ ์ ์๋ค.
private fun calculateExpense(): Pair<Int, Int> {
var total = 0
var mealExpenses = 0
for (expense in expenses) {
if (expense.type == Expense.Type.BREAKFAST || expense.type == Expense.Type.DINNER) {
mealExpenses += expense.amount
}
total += expense.amount
}
return Pair(total, mealExpenses)
}
์์ ๊ฐ์ด Pair๋ฅผ ํตํด์ ๋ถ๋ฆฌ๋ฅผ ํด์ฃผ๋ ค๊ณ ํ๋ค. Pair์ ๊ฒฝ์ฐ first, second ๋ฅผ ํตํด์ ๊ฐ๊ฐ์ ๋ณ์์ ์ ๊ทผํ ์ ์์ง๋ง, ์ฝ๋ ์ฌ๋์ ์ ์ฅ์์ ๊ฐ ๋ณ์๊ฐ ๋ฌด์์ ๋ปํ๋์ง ์ธ์งํ๊ธฐ๊ฐ ์ด๋ ค์์ง๋ค.
๊ทธ๋์, ์ด ๋ฐฉ๋ฒ ๋์ ์ total, mealExpenses์ ๋ณ์๋ฅผ ํด๋์ค์ ๋ณ์๋ก ์ฎ๊ฒจ์, ์ค์ง์ ์ผ๋ก ํด๋น ํจ์์๋ ํด๋์ค์ ๋ณ์๋ฅผ ์ ๊ทผํ์ฌ ๊ฐ์ ๊ณ์ฐํ ์ ์๋๋ก ๋ง๋ค์ด๋ณด์.
์ด๋, ์๋ฐ์์๋ ํด๋น ๋ณ์์ ์ปค์๋ฅผ ๋๊ณ command + option + F (extract field)๋ฅผ ๋๋ฅด๊ฒ ๋๋ฉด, ์๋์ ๊ฐ์ด ํด๋น ๋ณ์๋ฅผ ์ด๋๋ก ์ฎ๊ธธ์ง ์ ํํ ์ ์๋ ์ฐฝ์ด ๋จ๊ฒ ๋๋ค.
ํ์ง๋ง, ์ฝํ๋ฆฐ์์๋ var ๋ณ์์ ๊ฒฝ์ฐ ์ ๋์ํ์ง ์์์ ์๋์ผ๋ก ์ฎ๊ฒจ์ฃผ์๋ค. (val ๋ณ์๋ฉด ๊ด์ฐฎ์ ๊ฒ ๊ฐ์๋ฐ, var ๋ณ์์ฌ์ ์ ์ ์ฉ์ด ์ ๋ ๊ฒ ๊ฐ์์ ์์ฝ๋ค.)
class ExpenseReport {
private var total = 0
private var mealExpenses = 0
....
}
์ด์ , ๋น์ฆ๋์ค ๋ก์ง์ ๋ํด์ ํจ์๋ก ๋ถ๋ฆฌ๋ฅผ ํด๋ณด์. ๋น์ฉ์ ๋ํ ๊ณ์ฐ์ ์งํํ๊ณ ์์ผ๋, calculateExpense ๋ผ๋ ์ด๋ฆ์ ๋ถ์๋ค.
private fun calculateExpenses() {
for (expense in expenses) {
if (expense.type == Expense.Type.BREAKFAST || expense.type == Expense.Type.DINNER) {
mealExpenses += expense.amount
}
total += expense.amount
}
}
์ด ํจ์๋ ํ์ฌ if๋ฌธ์ด ํ๋ ์ผ์ ์ ํํ๊ฒ ์๊ธฐ ์ด๋ ค์ฐ๋, ํด๋น ๋ถ๋ถ๋ ํจ์๋ก ๋ถ๋ฆฌํด๋ณด์.
๋น์ฉ์ ํ์ ์ด ์์นจ์ด๋ ์ ๋ ์ธ ๊ฒฝ์ฐ mealExpenses๋ฅผ ๋ํ๊ณ ์์ผ๋, ๊ฐ๋จํ๊ฒ isMeal ์ด๋ผ๋ ๋ค์ด๋ฐ์ ๋ถ์ฌ์ฃผ์๋ค.
private fun calculateExpenses() {
for (expense in expenses) {
if (isMeal(expense)) {
mealExpenses += expense.amount
}
total += expense.amount
}
}
private fun isMeal(expense: Expense) = expense.type == Expense.Type.BREAKFAST || expense.type == Expense.Type.DINNER
์ฌ์ ํ ํจ์๊ฐ 2 depth ์ฌ์ ์์๋ณด๊ธฐ๊ฐ ์ด๋ ต๋ค. ํ ๋ฒ ๋ ๋ถ๋ฆฌ๋ฅผ ํด์ฃผ์.
private fun calculateExpenses() {
for (expense in expenses) {
addTotal(expense)
}
}
private fun addTotal(expense: Expense) {
if (isMeal(expense)) {
mealExpenses += expense.amount
}
total += expense.amount
}
๋ง์ฐฌ๊ฐ์ง๋ก, ์ถ๋ ฅ์ ๋ํ ๋ถ๋ถ๋ ํจ์๋ก ๋ถ๋ฆฌํด๋ณด์.
private fun printExpenses(printer: ReportPrinter) {
for (expense in expenses) {
var name = "TILT"
when (expense.type) {
Expense.Type.DINNER -> name = "Dinner"
Expense.Type.BREAKFAST -> name = "Breakfast"
Expense.Type.CAR_RENTAL -> name = "Car Rental"
}
printer.print(
String.format(
"%s\t%s\t$%.02f\n",
if (expense.type == Expense.Type.DINNER
&& expense.amount > 5000
|| expense.type === Expense.Type.BREAKFAST
&& expense.amount > 1000
) "X" else " ",
name, expense.amount / 100.0
)
)
}
}
์ ํจ์์์๋ ํฌ๊ฒ 2๊ฐ์ง๋ก ๋ถ๋ฆฌํ ์ ์๊ฒ ๋๋ค.
1. ๋น์ฉ์ ๋ํ ์ด๋ฆ์ ๊ณ์ฐํ๊ธฐ
2. ์ค์ง์ ์ผ๋ก ๋น์ฉ์ ๋ํ ์ด๋ฆ์ ์ถ๋ ฅํ๊ธฐ
์ด์ ๋ฐ๋ผ์ ํจ์๋ฅผ ๋ 2๊ฐ๋ก ๋ถ๋ฆฌํ์๋ค.
์ด๋, name์ ๊ฒฝ์ฐ ํจ์๋ก๋ถํฐ ์ป์ด์ค๊ฒ ๋๊ธฐ ๋๋ฌธ์ ๋ ์ด์ var ๋ก ์์ ํ์๊ฐ ์๊ธฐ ๋๋ฌธ์ val๋ก ๋ณ๊ฒฝํด์ฃผ์๋ค.
private fun printExpenses(printer: ReportPrinter) {
for (expense in expenses) {
val name = getName(expense)
printer.print(
String.format(
"%s\\t%s\\t$%.02f\\n",
if (expense.type == Expense.Type.DINNER
&& expense.amount > 5000
|| expense.type === Expense.Type.BREAKFAST
&& expense.amount > 1000
) "X" else " ",
name, expense.amount / 100.0
)
)
}
}
private fun getName(expense: Expense): String {
var name = "TILT"
when (expense.type) {
Expense.Type.DINNER -> name = "Dinner"
Expense.Type.BREAKFAST -> name = "Breakfast"
Expense.Type.CAR_RENTAL -> name = "Car Rental"
}
return name
}
์ฌ๊ธฐ๊น์ง ์งํํ๊ณ , ๋ง์ฐฌ๊ฐ์ง๋ก ํ ์คํธ ์ฝ๋๋ฅผ ํตํด ์ค๊ฐ์ค๊ฐ ์ฒดํฌ๋ฅผ ์งํํด์ฃผ์.
๐ฑ Step3 - ๋ถํ์ํ ๋ฉ์๋ ํ๋ผ๋ฏธํฐ ์ ๊ฑฐํ๊ธฐ
Step 1, 2๋ฅผ ์งํํ๋ฉด์ ๊ฐ์ฅ ๋ฉ์ธ์ด ๋๋ฉด public ํจ์์ธ printReport() ๊ฐ ๊ฐ๊ฒฐํด์ง ๊ฒ์ ํ์ธํ ์ ์๋ค.
class ExpenseReport {
private val expenses: MutableList<Expense> = ArrayList()
private val date: String
get() = "9/12/2002"
private var total = 0
private var mealExpenses = 0
fun printReport(printer: ReportPrinter) {
printHeader(printer)
calculateExpenses()
printExpenses(printer)
printTotal(printer, mealExpenses, total)
}
private fun printExpenses(printer: ReportPrinter) {
for (expense in expenses) {
val name = getName(expense)
printer.print(
String.format(
"%s\t%s\t$%.02f\n",
if (expense.type == Expense.Type.DINNER
&& expense.amount > 5000
|| expense.type === Expense.Type.BREAKFAST
&& expense.amount > 1000
) "X" else " ",
name, expense.amount / 100.0
)
)
}
}
private fun getName(expense: Expense): String {
var name = "TILT"
when (expense.type) {
Expense.Type.DINNER -> name = "Dinner"
Expense.Type.BREAKFAST -> name = "Breakfast"
Expense.Type.CAR_RENTAL -> name = "Car Rental"
}
return name
}
private fun calculateExpenses() {
for (expense in expenses) {
addTotal(expense)
}
}
private fun addTotal(expense: Expense) {
if (isMeal(expense)) {
mealExpenses += expense.amount
}
total += expense.amount
}
private fun isMeal(expense: Expense) = expense.type == Expense.Type.BREAKFAST || expense.type == Expense.Type.DINNER
private fun printTotal(printer: ReportPrinter, mealExpenses: Int, total: Int) {
printer.print(String.format("\nMeal expenses $%.02f", mealExpenses / 100.0))
printer.print(String.format("\nTotal $%.02f", total / 100.0))
}
private fun printHeader(printer: ReportPrinter) {
printer.print("Expenses " + date + "\n")
}
fun addExpense(expense: Expense) {
expenses.add(expense)
}
}
ํฌ๊ฒ ํค๋๋ฅผ ์ถ๋ ฅํ๊ธฐ / ๋น์ฉ์ ๊ณ์ฐํ๊ธฐ / ๋น์ฉ์ ์ถ๋ ฅํ๊ธฐ / ์ด ํฉ๊ณ๋ฅผ ์ถ๋ ฅํ๊ธฐ, ์ด๋ ๊ฒ 4๊ฐ์ง๋ก ๋ถ๋ฆฌ๋์๋ค.
๊ทธ๋ฌ๋, ํจ์๋ก ์ถ์ถํ๋ฉด์ parameter drilling ์ผ๋ก ์ธํด ํ์๊ฐ ์์์๋ ์ธ์๋ก ๊ณ์ ๋๊ฒจ์ฃผ๋ ๋ถ๋ถ๋ค์ด ๋์ ๋ค์ด์ฌ ๊ฒ์ด๋ค.
์ด๋ฒ ๋จ๊ณ์์๋ ํด๋น ๋ถ๋ถ์ ๋ํด ์ ๊ฑฐํด๋ณด๊ณ ์ ํ๋ค.
๊ฐ์ฅ ๋์ ๋๋ ๊ฒ์ ReportPrinter ๊ฐ ๊ณ์ํด์ ์ธ์๋ก ๋์ด๊ฐ๊ณ ์๋ค๋ ์ ์ด๋ค.
ํด๋น ๋ถ๋ถ์ ์ ๊ฑฐํ๊ธฐ ์ํด, ๋ง์ฐฌ๊ฐ์ง๋ก ReportPrinter์ ๋ํด ํด๋์ค์ ๋ณ์๋ก ์น๊ฒฉ์ํค๋ ์์ ์ ์งํํด๋ณด์.
๋ง์ฐฌ๊ฐ์ง๋ก ์๋ฐ ์ฝ๋์๋ค๋ฉด command + option + F ๋ฅผ ํตํด์ ์ ์ถ์ถ์ด ๋์ง๋ง, ์ฝํ๋ฆฐ์ด๊ธฐ ๋๋ฌธ์ ์ฐ์ ์๊ธฐ๋ก ์ถ์ถ์ ์งํํด์ฃผ์๋ค. ์ด๋, printer์ ๊ฒฝ์ฐ NPE ๋ฐฉ์ง๋ฅผ ์ํด์ lateinit ์ ํตํด ์ง์ฐ ์ด๊ธฐํ๊ฐ ๊ฐ๋ฅํ๋๋ก ์ค์ ํด์ฃผ์๋ค.
private lateinit var printer: ReportPrinter
fun printReport(printer: ReportPrinter) {
this.printer = printer
printHeader(printer)
calculateExpenses()
printExpenses(printer)
printTotal(printer, mealExpenses, total)
}
์ด๋ฌ๋ฉด, ํด๋์ค์ ํ๋ ๋ ๋ฒจ์์ printer๊ฐ ์ ์ธ์ด ๋์ด ์๊ธฐ ๋๋ฌธ์ ๋ ์ด์ ์ธ์๋ก ๊ณ์ ๋๊ฒจ์ค ํ์๊ฐ ์์ด์ง๋ค.
์๋ฐ๋ผ๋ฉด ๋์์ด ๋๋ ํจ์์์ command + option + n (Inline Parameter) ์ ๋๋ฅด๋ฉด ์์ ๊ฐ์ด ํด๋์ค์ ๋ณ์๋ก ์ ์ธ๋ ๊ฐ์ ์ฌ์ฉํ๋๋ก ์ฝ๊ฒ ๋ฆฌํฉํฐ๋ง์ด ๊ฐ๋ฅํ๋ค.
ํ์ง๋ง, ์ฝํ๋ฆฐ์ด๋ผ์ ๊ทธ๋ฐ์ง ์ ๋์ํ์ง ์์ command + F6 (Change Signature) ๋ฅผ ํตํด์ ์ธ์๋ก ๋์ด์จ printer ๋ฅผ ์ ๊ฑฐํด์ฃผ์๋ค. printer๋ฅผ ์ธ์๋ก ๋ฐ๋ ํจ์๋ค์ ๋ํด์ ๋ชจ๋ ์ ์ฉํด๋ณด๋๋ก ํ์.
์ฐธ๊ณ ๋ก ๋ด๋ถ์ ์ผ๋ก ์ง์ธ ๋๋ command + backspace๋ฅผ ํ์ฉํ๊ณ command + enter๋ฅผ ๋๋ฅด๋ฉด ๋ณ๊ฒฝ๋ ๋ด์ฉ์ด ์ ์ฉ๋๋ค. (์ด๋ฐ ๋ถ๋ถ์ ๋ํด์๋ ํฐ์นํจ๋๋ฅผ ์ฌ์ฉํ์ง ์๋ ๋ ธ๋ ฅ์ ํ๋ ๊ฒ์ด ์ข์ ๊ฒ ๊ฐ๋ค.)
์ด ๊ณผ์ ์์ conflict view๊ฐ ๋์ค๋ฉด continue๋ก ํ๊ฒ์ ์ฎ๊ธด ๋ค์ space๋ฅผ ๋๋ ค์ฃผ๋ฉด ์ ์ฉ๋๋ค!
๊ทธ๋ผ ์ฌ๊ธฐ๊น์ง ํ์ ๋ ์๋์ ๊ฐ์ ๋ชจ์ต์ด ๋์ฌ ๊ฒ์ด๋ฉฐ, ํ ๋ฒ ๋ ํ ์คํธ ์ฝ๋๋ฅผ ๋๋ ค ์ฒดํฌํด์ฃผ์.
cf) ํ ์คํธ ์ฝ๋๋ฅผ ์คํํ ๋๋ control + r (๋ฐ๋ก ์ด์ ์ ์คํ ๋ด์ญ ์ฌ์คํ) ์ ํ๊ฑฐ๋, control + option + r์ ํตํด์ ์คํ ๋ด์ญ ์ค์์ ์ํ๋ ๊ฒ์ ์คํํ ์ ์๋๋ก ํด๋ณด์.
class ExpenseReport {
private val expenses: MutableList<Expense> = ArrayList()
private val date: String
get() = "9/12/2002"
private var total = 0
private var mealExpenses = 0
private lateinit var printer: ReportPrinter
fun printReport(printer: ReportPrinter) {
this.printer = printer
printHeader()
calculateExpenses()
printExpenses()
printTotal()
}
private fun printExpenses() {
for (expense in expenses) {
val name = getName(expense)
printer.print(
String.format(
"%s\t%s\t$%.02f\n",
if (expense.type == Expense.Type.DINNER
&& expense.amount > 5000
|| expense.type === Expense.Type.BREAKFAST
&& expense.amount > 1000
) "X" else " ",
name, expense.amount / 100.0
)
)
}
}
private fun getName(expense: Expense): String {
var name = "TILT"
when (expense.type) {
Expense.Type.DINNER -> name = "Dinner"
Expense.Type.BREAKFAST -> name = "Breakfast"
Expense.Type.CAR_RENTAL -> name = "Car Rental"
}
return name
}
private fun calculateExpenses() {
for (expense in expenses) {
addTotal(expense)
}
}
private fun addTotal(expense: Expense) {
if (isMeal(expense)) {
mealExpenses += expense.amount
}
total += expense.amount
}
private fun isMeal(expense: Expense) = expense.type == Expense.Type.BREAKFAST || expense.type == Expense.Type.DINNER
private fun printTotal() {
printer.print(String.format("\nMeal expenses $%.02f", mealExpenses / 100.0))
printer.print(String.format("\nTotal $%.02f", total / 100.0))
}
private fun printHeader() {
printer.print("Expenses " + date + "\n")
}
fun addExpense(expense: Expense) {
expenses.add(expense)
}
}
์ฌ๊ธฐ์์, printReport()์ ํจ์๋ฅผ ํ ๋ฒ ๋ ๋ถ๋ฆฌํ์ฌ ์์ ํ๊ฒ ๊ณ์ฐ๊ณผ ์ถ๋ ฅ์ ๋ํ ๋ถ๋ถ์ผ๋ก ๋๋์.
fun printReport(printer: ReportPrinter) {
this.printer = printer
calculateExpenses()
printExpensesAndTotal()
}
private fun printExpensesAndTotal() {
printHeader()
printExpenses()
printTotal()
}
printExpenses() ์ญ์ ํจ์๊ฐ 2 depth ๋ฅผ ๋์ด๊ฐ๋ ์์๋ณด๊ธฐ ์ด๋ ค์ด ๊ฒ ๊ฐ๋ค. ํ ๋ฒ ๋ ๋ถ๋ฆฌํด์ฃผ์.
private fun printExpenses() {
for (expense in expenses) {
printExpense(expense)
}
}
private fun printExpense(expense: Expense) {
val name = getName(expense)
printer.print(
String.format(
"%s\t%s\t$%.02f\n",
if (expense.type == Expense.Type.DINNER
&& expense.amount > 5000
|| expense.type === Expense.Type.BREAKFAST
&& expense.amount > 1000
) "X" else " ",
name, expense.amount / 100.0
)
)
}
printExpense() ๋ด๋ถ์์ name ์ด ํ ๊ณณ์์๋ง ์ฐ์ด๊ณ ์์ผ๋ inline ์ ํตํด์ ์กฐ๊ธ ๋ ์ค์ฌ์ค ์ ์๋ค.
private fun printExpense(expense: Expense) {
printer.print(
String.format(
"%s\t%s\t$%.02f\n",
if (expense.type == Expense.Type.DINNER
&& expense.amount > 5000
|| expense.type === Expense.Type.BREAKFAST
&& expense.amount > 1000
) "X" else " ",
getName(expense), expense.amount / 100.0
)
)
}
์ถ๊ฐ์ ์ผ๋ก ๊ฐ์ ํฌ๋งทํ ํ๋ ๋ถ๋ถ์ ๊ฐ๋ ์ฑ์ ์ํด if๋ฌธ์ ํ ๋ฒ ๋ ํจ์๋ก ๋ถ๋ฆฌํด์ฃผ์.
private fun printExpense(expense: Expense) {
printer.print(
String.format(
"%s\t%s\t$%.02f\n",
if (isOverage(expense)) "X" else " ",
getName(expense), expense.amount / 100.0
)
)
}
private fun isOverage(expense: Expense) = (expense.type == Expense.Type.DINNER
&& expense.amount > 5000
|| expense.type === Expense.Type.BREAKFAST
&& expense.amount > 1000)
๋ง์ง๋ง์ผ๋ก, ๊ธฐ์กด ์ฝ๋์์๋ 100์ผ๋ก ๋๋๋ ๋ถ๋ถ๋ค์ด ์๋นํ ๊ฒน์น๋ ๊ฒ์ ๋ณผ ์ ์์๋ค. ์ด ๋ถ๋ถ์ ๋ํด์ ํจ์๋ก ์ถ์ถํด๋ณด์.
์ด๋, ํ์ฌ ์ํ์์ ๋จ์ํ๊ฒ ํจ์๋ก ์ถ์ถํ๊ฒ ๋๋ฉด ๊ฒน์น๋ ๋ถ๋ถ๋ค์ ๋ํด intellij ๊ฐ ์ถ์ฒ์ ํด์ฃผ์ง ์๊ธฐ ๋๋ฌธ์, 1์ฐจ์ ์ผ๋ก ์ง์ญ๋ณ์๋ก ๋ถ๋ฆฌ๋ฅผ ํด๋ณด์. (์ด์ฐจํผ ์ฌ๋ผ์ง ๋ณ์๋ค์ด๊ธฐ ๋๋ฌธ์ ์ด๋ฆ์ ๋์ถฉ ์ง์๋ค.)
private fun printExpense(expense: Expense) {
val amount = expense.amount
printer.print(
String.format(
"%s\\t%s\\t$%.02f\\n",
if (isOverage(expense)) "X" else " ",
getName(expense), amount / 100.0
)
)
}
private fun printTotal() {
val tempMealExpense = mealExpenses
val tempTotal = total
printer.print(String.format("\\nMeal expenses $%.02f", tempMealExpense / 100.0))
printer.print(String.format("\\nTotal $%.02f", tempTotal / 100.0))
}
์ด ์ํ์์ extract method ๋ฅผ ํ๊ฒ ๋๋ฉด, ํจ์์ ์๊ทธ๋์ฒ๊ฐ Int → Double ๋ก ๊ณ ์ ๋๊ธฐ ๋๋ฌธ์ ์ฌ์ฉ๋๋ ๋ชจ๋ ๋ถ๋ถ์ ๋ํด์ ์ ๋ถ ๊ต์ฒด๊ฐ ๊ฐ๋ฅํด์ง๋ค.
private fun printExpense(expense: Expense) {
val amount = expense.amount
printer.print(
String.format(
"%s\t%s\t$%.02f\n",
if (isOverage(expense)) "X" else " ",
getName(expense), getRate(amount)
)
)
}
private fun printTotal() {
val tempMealExpense = mealExpenses
val tempTotal = total
printer.print(String.format("\nMeal expenses $%.02f", getRate(tempMealExpense)))
printer.print(String.format("\nTotal $%.02f", getRate(tempTotal)))
}
private fun getRate(amount: Int) = amount / 100.0
๋ง์ง๋ง์ผ๋ก inline variable ์ ํตํด์ ์์ ๋ณ์๋ฅผ ์ ๊ฑฐํด์ฃผ์.
๊ทธ๋ผ ์ต์ข ์ ์ผ๋ก ์ฐ๋ฆฌ๊ฐ ์ํ๋ ํํ๋ก ์ ์์ ๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
private fun printExpense(expense: Expense) {
printer.print(
String.format(
"%s\t%s\t$%.02f\n",
if (isOverage(expense)) "X" else " ",
getName(expense), getRate(expense.amount)
)
)
}
private fun printTotal() {
printer.print(String.format("\nMeal expenses $%.02f", getRate(mealExpenses)))
printer.print(String.format("\nTotal $%.02f", getRate(total)))
}
private fun getRate(amount: Int) = amount / 100.0
๐ฑ Step4 - ๋๋ฉ์ธ์ ์์ง๋๋ฅผ ๋์ฌ๋ณด์
์ฌ๊ธฐ๊น์ง ํ๊ณ ๋๋ฉด ์ ์ฒด์ ์ธ ํด๋์ค์ ๋ํด ์ฝ๊ธฐ ์ฌ์ด ์ฝ๋๊ฐ ๋์ด ์์ ๊ฒ์ด๋ค.
ํ์ง๋ง, ํต์ฌ ๋๋ฉ์ธ์ธ Expense ๊ฐ ํ๋ ์ผ์ด ๊ต์ฅํ ๋น์ฝํ๊ฒ ๋๊ปด์ง๋ค.
์ถ๋ ฅ๊ณผ ํด๋์ค์ ์ ์ญ ๋ณ์์ ๊ด๋ จ์์ด ์ํ๋๊ณ ์๋ ‘๋น์ฉ์ ์ด๋ฆ์ ๋ํด์ ๊ฐ์ ธ์ค๋ ๊ธฐ๋ฅ’๊ณผ ‘meal์ธ์ง ํ๋จํ๋ ๊ธฐ๋ฅ’, ‘ํ๊ท ์ธ์ง ๊ณ์ฐํ๋ ๊ธฐ๋ฅ’์ ๋๋ฉ์ธ์๊ฒ ๋๊ฒจ์ค ์ ์์ง ์์๊น?
f6์ ๋๋ฌ (Move Method) ๋๋ฉ์ธ์๊ฒ ํด๋น ๊ธฐ๋ฅ์ ์์ํด๋ณด์.
์ฐธ๊ณ ๋ก, ์ฝํ๋ฆฐ์์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ด ๊ธฐ๋ฅ์ด ๊บผ์ ธ ์๊ธฐ ๋๋ฌธ์ ๋ณ๋์ ์ค์ ์ด ํ์ํ๋ค.
์๋์ ๊ธ์์ ์ค์ ํ๋ ๋ฐฉ๋ฒ์ ๋ฐํ์ผ๋ก ๋ฏธ๋ฆฌ ํด๋๋ฉด ์ข๋ค. (๊ฐ๋ง๊ท)
๊ทธ๋ฌ๋ฉด ์๋์ ๊ฐ์ด ๋๋ฉ์ธ์๊ฒ ํด๋น ๊ธฐ๋ฅ๋ค์ด ์ ๋์ด๊ฐ ๊ฒ์ ๋ณผ ์ ์๋ค.
class Expense(
var type: Type,
var amount: Int
) {
enum class Type {
DINNER,
BREAKFAST,
CAR_RENTAL
}
fun getName(): String {
var name = "TILT"
when (this.type) {
Type.DINNER -> name = "Dinner"
Type.BREAKFAST -> name = "Breakfast"
Type.CAR_RENTAL -> name = "Car Rental"
}
return name
}
fun isMeal() = this.type == Type.BREAKFAST || this.type == Type.DINNER
fun isOverage() = (this.type == Type.DINNER
&& this.amount > 5000
|| this.type === Type.BREAKFAST
&& this.amount > 1000)
}
๐ฑ Step5 - ์ถ๋ ฅํ๋ ๋ถ๋ถ์ ๋ํด์ ๋ถ๋ฆฌํด๋ณด์
์ฌ์ ํ ์ถ๋ ฅ๊ณผ ๋น์ฆ๋์ค ๋ก์ง์ด ํฉ์ณ์ ธ ์๋ค๋ ๋ถ๋ถ์ด ๋ง์์ ๊ฑธ๋ฆฐ๋ค.
๊ทธ๋์, ์ฐ๋ฆฌ๋ ๋น์ฉ์ ๊ณ์ฐํ๋ ๋ถ๋ถ๊ณผ ์ถ๋ ฅ์ ๋ด๋นํ๋ ๋ถ๋ถ์ ๋ณ๋์ ํด๋์ค๋ก ๋ถ๋ฆฌํ์ฌ ๊ธฐ๋ฅ๋ณ ์์ง๋๋ฅผ ๋์ฌ๋ณด๋๋ก ํ ๊ฒ์ด๋ค.
๊ทธ๋ฌ๋… ์๋ฐ์๋ control + t → extract delegate ๋ฅผ ์ ํํ๋ฉด ์ฝ๊ฒ ๋ถ๋ฆฌ๊ฐ ๊ฐ๋ฅํ์ง๋ง, ์ฝํ๋ฆฐ์๋ ์กด์ฌํ์ง ์๋๋ค. (๊ฐ์ฅ ์์ฝ๋ค)
๊ทธ๋์, extract delegate ๋์ extract superclass๋ฅผ ํ์ฉํ์ฌ ์ฝ๊ฐ์ ๊ผผ์๋ก ํด๋์ค๋ฅผ ๋ถ๋ฆฌํด๋ณด๊ณ ์ ํ๋ค.
๋จผ์ , ๊ธฐ์กด์ ํด๋์ค์ ์ด๋ฆ์ ExpenseReporter ๋ก ๋ณ๊ฒฝํ์ฌ ์ถ๋ ฅ์ ๋ํ ๊ธฐ๋ฅ๋ค๋ง ๋จ๊ธธ ์ ์๋๋ก ์๋ฏธ๋ฅผ ๋ถ์ฌํด์ฃผ์๋ค. (Shift + F6 - Rename)
๊ทธ๋ฆฌ๊ณ , extract superclass๋ฅผ ํตํด์ ๋น์ฉ์ ๊ณ์ฐํ๋ ๋ถ๋ถ์ ์ถ์ถํด์ฃผ์.
ํจ์๋ฅผ ์ ํํ๋ค ๋ณด๋ฉด ์์ ๊ฐ์ด !๊ฐ ๋ ์๋ ๊ฒ์ ๋ณผ ์ ์๋๋ฐ, ์ด๋ ๊ฐ์ด ์ฎ๊ฒจ์ผ ๊นจ์ง์ง ์๋ ์น๊ตฌ๋ค์ Intellj ๊ฐ ๊ฐ์งํ์ฌ ์๋ ค์ค๋ค.
!๊ฐ ๋ฌ ์น๊ตฌ๋ค๊น์ง ํจ๊ป ์ฒดํฌํ์ฌ ์ ํํด์ค๋ค.
๊ทธ๋ผ ์๋์ ๊ฐ์ด ExpenseReport ๋ผ๋ ์น๊ตฌ๊ฐ ์๊ธฐ๊ฒ ๋๋ค. ์ฐ๋ฆฌ๋ SuperClass๋ก ์ด์ฉ ์ ์์ด ์์ฑํ๊ธฐ ๋๋ฌธ์ open class๊ฐ ๋์๋๋ฐ, ํด๋น ํด๋์ค๋ฅผ ์ผ๋ฐ ํด๋์ค๋ก ๋ฐ๊พธ์ด์ฃผ๊ณ , ์ธ๋ถ์์ ํธ์ถํ ์ ์๋๋ก ํจ์ ์ญ์ public ์ผ๋ก ์ด์ด๋๋๋ก ํ์.
// AS-IS
open class ExpenseReport {
protected val expenses: MutableList<Expense> = ArrayList()
protected var total = 0
protected var mealExpenses = 0
protected fun calculateExpenses() {
for (expense in expenses) {
addTotal(expense)
}
}
private fun addTotal(expense: Expense) {
if (expense.isMeal()) {
mealExpenses += expense.amount
}
total += expense.amount
}
fun addExpense(expense: Expense) {
expenses.add(expense)
}
}
// TO-BE
class ExpenseReport {
val expenses: MutableList<Expense> = ArrayList()
var total = 0
var mealExpenses = 0
fun calculateExpenses() {
for (expense in expenses) {
addTotal(expense)
}
}
private fun addTotal(expense: Expense) {
if (expense.isMeal()) {
mealExpenses += expense.amount
}
total += expense.amount
}
fun addExpense(expense: Expense) {
expenses.add(expense)
}
}
๋ํ, ๊ธฐ์กด ํด๋์ค๋ ๊นจ์ ธ์์ ํ ๋๊น ์์ ์ด ํ์ํ๋ค. Report์ ๋ํด ์ ์ญ ๋ณ์๋ฅผ ์ ์ธํด์ค ๋ค์, F2๋ฅผ ๋๋ฌ ์ค๋ฅ ๋ถ๋ถ์ ์ฐพ์๊ฐ๋ฉฐ ์์ ํด์ฃผ์. (addExpense์ ๊ฒฝ์ฐ ํ ์คํธ ์ฝ๋๋ฅผ ์ต๋ํ ๋ ๊นจ์ง๊ฒ ํ๊ธฐ ์ํด์ ๋ค์ ๋ง๋ค์ด ๋์๋ค.)
class ExpenseReporter {
private val date: String
get() = "9/12/2002"
private lateinit var printer: ReportPrinter
private val expenseReport = ExpenseReport()
fun printReport(printer: ReportPrinter) {
this.printer = printer
expenseReport.calculateExpenses()
printExpensesAndTotal()
}
private fun printExpensesAndTotal() {
printHeader()
printExpenses()
printTotal()
}
private fun printExpenses() {
for (expense in expenseReport.expenses) {
printExpense(expense)
}
}
private fun printExpense(expense: Expense) {
printer.print(
String.format(
"%s\t%s\t$%.02f\n",
if (isOverage(expense)) "X" else " ",
expense.getName(), expense.amount / 100.0
)
)
}
private fun isOverage(expense: Expense) = (expense.type == Expense.Type.DINNER
&& expense.amount > 5000
|| expense.type === Expense.Type.BREAKFAST
&& expense.amount > 1000)
private fun printTotal() {
printer.print(String.format("\nMeal expenses $%.02f", expenseReport.mealExpenses / 100.0))
printer.print(String.format("\nTotal $%.02f", expenseReport.total / 100.0))
}
private fun printHeader() {
printer.print("Expenses " + date + "\n")
}
fun addExpense(expense: Expense) {
expenseReport.expenses.add(expense)
}
}
๐ฑ Step6 - OCP๋ฅผ ๊ฐ์ ํด๋ณด์
์ฌ๊ธฐ๊น์ง ์ ์คํํ๋ค๋ฉด ํด๋์ค๊ฐ ๊ฝค๋ ๊ฐ๊ฒฐํด์ง ๊ฒ์ ๋ณผ ์ ์์ ๊ฒ์ด๋ค.
// ์ถ๋ ฅ์ ๋ด๋นํ๋ ํด๋์ค
class ExpenseReporter {
private val date: String
get() = "9/12/2002"
private lateinit var printer: ReportPrinter
private val expenseReport: ExpenseReport = ExpenseReport()
fun printReport(printer: ReportPrinter) {
this.printer = printer
expenseReport.calculateExpenses()
printExpensesAndTotal()
}
private fun printExpensesAndTotal() {
printHeader()
printExpenses()
printTotal()
}
private fun printExpenses() {
for (expense in expenseReport.expenses) {
printExpense(expense)
}
}
private fun printExpense(expense: Expense) {
printer.print(
String.format(
"%s\t%s\t$%.02f\n",
if (expense.isOverage()) "X" else " ",
expense.getName(), getRate(expense.amount)
)
)
}
private fun printTotal() {
printer.print(String.format("\nMeal expenses $%.02f", getRate(expenseReport.mealExpenses)))
printer.print(String.format("\nTotal $%.02f", getRate(expenseReport.total)))
}
private fun getRate(amount: Int) = amount / 100.0
private fun printHeader() {
printer.print("Expenses " + date + "\n")
}
fun addExpense(expense: Expense) {
expenseReport.addExpense(expense)
}
}
// ๊ณ์ฐ์ ๋ด๋นํ๋ ํด๋์ค
class ExpenseReport {
val expenses: MutableList<Expense> = ArrayList()
var total = 0
var mealExpenses = 0
fun calculateExpenses() {
for (expense in expenses) {
addTotal(expense)
}
}
private fun addTotal(expense: Expense) {
if (expense.isMeal()) {
mealExpenses += expense.amount
}
total += expense.amount
}
fun addExpense(expense: Expense) {
expenses.add(expense)
}
}
// ๋น์ฉ์ ๋ํ ๋๋ฉ์ธ
class Expense(
var type: Type,
var amount: Int
) {
enum class Type {
DINNER,
BREAKFAST,
CAR_RENTAL
}
fun getName(): String {
var name = "TILT"
when (this.type) {
Type.DINNER -> name = "Dinner"
Type.BREAKFAST -> name = "Breakfast"
Type.CAR_RENTAL -> name = "Car Rental"
}
return name
}
fun isMeal() = this.type == Type.BREAKFAST || this.type == Type.DINNER
fun isOverage() = (this.type == Type.DINNER
&& this.amount > 5000
|| this.type === Type.BREAKFAST
&& this.amount > 1000)
}
์ฐ๋ฆฌ๋ ์ด์ ๋ง์ง๋ง์ผ๋ก, ์๋ก์ด Expense์ ํ์ ์ด ์ถ๊ฐ๋์์ ๋ ์ ์ฐํ๊ฒ ํ์ฅํ ์ ์๋๋ก ๊ฐ๊ฐ์ ๋ํด ํด๋์ค๋ก ๋ถ๋ฆฌํ๋ ์์ ์ ์งํํ ๊ฒ์ด๋ค.
๋จผ์ , ์ํํ ์์ ์ ์ํด์ Expense์ ํด๋์ค๋ฅผ ๋ฏธ๋ฆฌ abstract class๋ก ๋ฐ๊พธ์ด๋์. (์ด๋ ๊ฒ ํ์ง ์์ผ๋ฉด, ์๋์์ ์ธ๋ถ ํด๋์ค๋ค์ ๋ง๋ค ๋ ๋จ์ถํค๋ฅผ ์ฌ์ฉํ ์ ์๋ค.)
abstract class Expense(
var type: Type,
var amount: Int
)
๊ทธ๋ฆฌ๊ณ , ํ ์คํธ ์ฝ๋๋ฅผ ์์ ํ๋ ์์ ์ ์งํํด๋ณด์.
๊ธฐ์กด์ ํ ์คํธ ์ฝ๋์์ ๋น์ฉ ๊ฐ์ฒด๋ฅผ ์์ฑํ๊ธฐ ์ํด Dinner ํ์ ์ ๋๊ฒจ์ค ๋ถ๋ถ์, ์ ๋ ์ ๋ํ ๋น์ฉ์ ๋ํ๋ด๋ ์๋ก์ด ํด๋์ค๋ฅผ ๋ง๋ค์ด์ฃผ๊ธฐ ์ํด ์๋กญ๊ฒ ์์ฑํด์ค๋ค.
// AS-IS
"printOneDinner" {
report.addExpense(Expense(Expense.Type.DINNER, 1678))
report.printReport(printer)
printer.getText() shouldBe """
Expenses 9/12/2002
Dinner $16.78
Meal expenses $16.78
Total $16.78
""".trimIndent()
}
// TO-BE
"printOneDinner" {
report.addExpense(DinnerExpense(1678))
report.printReport(printer)
printer.getText() shouldBe """
Expenses 9/12/2002
Dinner $16.78
Meal expenses $16.78
Total $16.78
""".trimIndent()
}
๊ทธ๋ผ, ์ฐ๋ฆฌ๋ ์ด ํ ์คํธ๋ฅผ ๊นจ์ง์ง ์๋๋ก ๋ง๋๋ ๊ฒ์ด ๊ฐ์ฅ ์ค์ํ๋ค. DinnerExpense๋ผ๋ ํด๋์ค๋ฅผ ์๋กญ๊ฒ ๋ง๋ค์ด์ฃผ์.
(๋ง์น TDD๋ฅผ ํ๋ ๊ฒ์ฒ๋ผ, ํ ์คํธ๋ฅผ ์ค์ ์ผ๋ก ํ์ฌ ๋ฆฌํฉํฐ๋ง์ ์งํํ๋ ๊ฒ์ด๋ค.)
option + enter๋ฅผ ๋๋ฅด๋ฉด ์์ ๊ฐ์ด create class๋ฅผ ํตํด ๋ง๋ค์ด์ค ์ ์๋ค.
๊ฐ์ ๋ฐฉ๋ฒ์ผ๋ก ๋๋จธ์ง ํ์ ์ ๋ํ ํด๋์ค๋ ๋ง๋ค์ด์ฃผ์.
์ด๋ฏธ ํด๋์ค์์ ์ด๋ค ํ์ ์ธ์ง ๋๋ฌ๋๊ณ ์์ด, ์ธ์๋ก type ์กฐ๊ฑด์ ๋๊ฒจ์ฃผ๋ ๊ฒ์ด ์ด์ํ๊ธฐ ๋๋ฌธ์ ํด๋์ค ์์ฑ ํ change Signature๋ฅผ ํตํด ์ผ๊ด์ ์ผ๋ก ์ ๊ฑฐํด์ฃผ์๋ค.
class DinnerExpense(amount: Int) : Expense(Type.DINNER, amount)
class BreakfastExpense(amount: Int) : Expense(Type.BREAKFAST, amount)
class CarRentalExpense(amount: Int) : Expense(Type.CAR_RENTAL, amount)
๊ทธ๋ผ, ํ ์คํธ ์ฝ๋ ์ญ์ ์๋์ ๊ฐ์ด ๋ฐ๋์์ ๊ฒ์ด๋ค.
internal class ExpenseReportTest : StringSpec({
lateinit var report: ExpenseReporter
lateinit var printer: MockReportPrinter
beforeTest {
report = ExpenseReporter()
printer = MockReportPrinter()
}
"printEmpty" {
report.printReport(printer)
printer.getText() shouldBe """
Expenses 9/12/2002
Meal expenses $0.00
Total $0.00
""".trimIndent()
}
"printOneDinner" {
report.addExpense(DinnerExpense(1678))
report.printReport(printer)
printer.getText() shouldBe """
Expenses 9/12/2002
Dinner $16.78
Meal expenses $16.78
Total $16.78
""".trimIndent()
}
"twoMeals" {
report.addExpense(DinnerExpense(1000))
report.addExpense(BreakfastExpense(500))
report.printReport(printer)
printer.getText() shouldBe """
Expenses 9/12/2002
Dinner $10.00
Breakfast $5.00
Meal expenses $15.00
Total $15.00
""".trimIndent()
}
"twoMealsAndCarRental" {
report.addExpense(DinnerExpense(1000))
report.addExpense(BreakfastExpense(500))
report.addExpense(CarRentalExpense(50000))
report.printReport(printer)
printer.getText() shouldBe """
Expenses 9/12/2002
Dinner $10.00
Breakfast $5.00
Car Rental $500.00
Meal expenses $15.00
Total $515.00
""".trimIndent()
}
"overages" {
report.addExpense(BreakfastExpense(1000))
report.addExpense(BreakfastExpense(1001))
report.addExpense(DinnerExpense(5000))
report.addExpense(DinnerExpense(5001))
report.printReport(printer)
printer.getText() shouldBe """
Expenses 9/12/2002
Breakfast $10.00
X Breakfast $10.01
Dinner $50.00
X Dinner $50.01
Meal expenses $120.02
Total $120.02
""".trimIndent()
}
})
๐ฑ Step7 - ์์ ํด๋์ค๋ค์๊ฒ ์ฑ ์์ ๋ถ๋ฆฌํ์
๊ฐ ํด๋์ค๋ค์ ๋ง๋ค์ด์ฃผ์์ง๋ง, ๊นกํต ํด๋์ค์ด๊ธฐ ๋๋ฌธ์ ๊ต์ฅํ ๋ถ์คํ๋ค.
์ฐ๋ฆฌ์ Expense ํด๋์ค๋ฅผ ๋๋์๋ณด์. ์ฌ๊ธฐ์์ ๊ฐ ํจ์๋ค์ ์์ ํด๋์ค๋ค๋ก ๋ด๋ ค๊ฐ๊ธฐ์ ์ข์ ๋ณด์ธ๋ค.
abstract class Expense(
var type: Type,
var amount: Int
) {
enum class Type {
DINNER,
BREAKFAST,
CAR_RENTAL
}
fun getName(): String {
var name = "TILT"
when (this.type) {
Type.DINNER -> name = "Dinner"
Type.BREAKFAST -> name = "Breakfast"
Type.CAR_RENTAL -> name = "Car Rental"
}
return name
}
fun isMeal() = this.type == Type.BREAKFAST || this.type == Type.DINNER
fun isOverage() = (this.type == Type.DINNER
&& this.amount > 5000
|| this.type === Type.BREAKFAST
&& this.amount > 1000)
}
์ด๋ฅผ ์ํด์, ๊ฐ๊ฐ์ ํจ์๋ฅผ ์์ ํด๋์ค๋ค์๊ฒ ๋๊ฒจ์ฃผ๊ธฐ ์ํด control + t → push member down์ ๋๋ฌ์ฃผ์.
๊ทธ๋ฌ๋ฉด, ์๋์ ๊ฐ์ด Expense ํด๋์ค์ ๊ฐ ๋ฉ์๋๋ค์ด abstract ํด๋์ค๋ก ๋ณํ๊ฒ ๋๋ค.
abstract class Expense(
var type: Type,
var amount: Int
) {
enum class Type {
DINNER,
BREAKFAST,
CAR_RENTAL
}
abstract fun getName(): String
abstract fun isMeal(): Boolean
abstract fun isOverage(): Boolean
}
์ด์ , ๊ฐ๊ฐ์ ํด๋์ค๋ค์๊ฒ ๊ฐ์ ์ด์ธ๋ฆฌ๋ ํ๋์ ํ ์ ์๋๋ก ์์ ํด์ฃผ์.
// AS-IS
class BreakfastExpense(amount: Int) : Expense(Type.BREAKFAST, amount) {
override fun getName(): String {
var name = "TILT"
when (this.type) {
Type.DINNER -> name = "Dinner"
Type.BREAKFAST -> name = "Breakfast"
Type.CAR_RENTAL -> name = "Car Rental"
}
return name
}
override fun isMeal() = this.type == Type.BREAKFAST || this.type == Type.DINNER
override fun isOverage() = (this.type == Type.DINNER
&& this.amount > 5000
|| this.type === Type.BREAKFAST
&& this.amount > 1000)
}
// TO-BE
class BreakfastExpense(amount: Int) : Expense(Type.BREAKFAST, amount) {
override fun getName(): String = "Breakfast"
override fun isMeal() = true
override fun isOverage() = this.amount > 1000
}
// ๋๋จธ์ง๋ ๋์ผํ๊ฒ ๋ณ๊ฒฝ
class CarRentalExpense(amount: Int) : Expense(Type.CAR_RENTAL, amount) {
override fun getName(): String = "Car Rental"
override fun isMeal() = false
override fun isOverage() = false
}
class DinnerExpense(amount: Int) : Expense(Type.DINNER, amount) {
override fun getName(): String = "Dinner"
override fun isMeal() = true
override fun isOverage() = this.amount > 5000
}
๋ํ, ์ฌ๊ธฐ์์ ๊ธฐ์กด์ ์ ์ํด์ฃผ์๋ Expense ํด๋์ค ๋ด๋ถ์ Type์ด๋ผ๋ enum class๋ getName์ ์ํด์ ์ฌ์ฉ๋์๋ ํด๋์ค์ด๊ธฐ ๋๋ฌธ์ ์ ๊ฑฐํด๋ ๋ฌด๋ฆฌ๊ฐ ์๋ค. ์ง์์ฃผ๋๋ก ํ์. ๋ง์ฐฌ๊ฐ์ง๋ก ์ด์ ์ ์ฌ์ฉํ๋ command + F6 (Change Signature)๋ฅผ ์ฌ์ฉํ์. ์ด๋ ๊ฒ ํ๋ฉด ์์ ํด๋์ค์ ๊ฐ์ ์ง์ ์ ๊ฑฐํด์ค ํ์๊ฐ ์์ด ํธํ๊ฒ ์ง์ธ ์ ์๋ค.
์ฌ๊ธฐ๊น์ง ์์ ํ ์คํธ ์ฝ๋๋ฅผ ํ ๋ฒ ๋๋ ค๋ณด๋๋ก ํ์. ๋ค ๋์๊ฐ๋ค๋ฉด ์ฑ๊ณต์ด๋ค.
๐ฑ ๋ง๋ฌด๋ฆฌ
์ต์ข ์ ์ผ๋ก ์์ ๊ฐ์ ๊ด๊ณ๋๋ฅผ ๊ฐ์ง๋ ๊ฒ์ ๋ณผ ์ ์์ผ๋ฉฐ, ์ด์ ๊ณผ ๋น๊ตํ์ ๋ ์ฝ๋ ์ญ์ ํจ์ฌ ์ฝ๊ธฐ ์ข๊ณ ๊น๋ํ ์ฝ๋๋ก ๋ณ๊ฒฝ๋์๋ค.
๊ธ์ด ๊ต์ฅํ ๊ธธ์ด์ก๊ธฐ๋ ํ๊ณ , ์ค์ ๋ก ๋ฆฌํฉํฐ๋ง์ ์งํํ๋๋ฐ๋ ๊ฝค ์ค๋ ๊ฑธ๋ ธ๋ค.
์ด๋ฒ ๋ฆฌํฉํฐ๋ง์ ํต์ฌ์, ์ด๋ป๊ฒ ํ๋ฉด ์์ ํ๊ฒ ๋ฆฌํฉํฐ๋ง์ ํ ์ ์๋์ง๊ฐ ์ค์ ์ด ๋์๋ค.
์ฒ์์ ์๋ณธ ์ฝ๋๋ฅผ ๋ณด์์ ๋๋ ์ฝ๋์ ์ค๊ณ ๊ด์ ๋ง ์๊ฐํด์ ์๊ธฐ๋ก ํด๋์ค๋ฅผ ๋ง๋ค๊ณ ๋ถ๋ฆฌํ๋ ์ผ๋ค์ ๋ง์ด ํ์๋๋ฐ, ์ด๋ฒ ๋ฆฌํฉํฐ๋ง์ ๊ฐ ๋จ๊ณ๋ง๋ค ์ฒ์ฒํ ์งํํ๋ฉด์ ๋๊ผ๋ ๊ฑด Intellij ์์ ์๊ฐ๋ณด๋ค ๋ง์ ๊ธฐ๋ฅ์ ์ง์ํด์ฃผ๊ณ ์์๋ค๋ ์ ์ด์๋ค.
๋ํ, ์ค๋ฌด์์ ๋ฆฌํฉํฐ๋ง์ ํ๋ค ๋ณด๋ฉด ์ ๋ง ๋ง์ ํด๋์ค์ ๋ํด์ ๋ฐ๋ณต์ ์ผ๋ก ๊ฐ์ ์์ ์ ํ๋ ๊ฒ๋ค์ด ๋ง์์ ๋ณต์ฌ ๋ถ์ฌ๋ฃ๊ธฐ๋ฅผ ํ๋๋ผ ์๊ฐ์ ์์ฒญ ์ผ์๋๋ฐ, ์์ผ๋ก๋ ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณตํ๋ ๊ธฐ๋ฅ์ ๋จผ์ ์ฐพ์๋ณด๊ณ ์จ์ผ๊ฒ ๋ค๋ ์๊ฐ์ด ๋ค์๋ค. (์์ด๋ฒ๋ฆฐ ๋ด ์๊ฐ๋ค...)
๊ทธ๋ฆฌ๊ณ ๋ฌด์๋ณด๋ค ์ค์ํ ๊ฑด, ๊ธฐ๋ฅ์ ์์ ํ์ ๋ ๋ถ์ํ์ง ์๋๋ก ํ ์คํธ ์ฝ๋๋ฅผ ๊ผญ ์์ฑํด์ผ ํ๋ค๋ ์ ์ด๋ค.
์ผ์ ์งํํ๋ฉด์ ์ฝ๋ ๋์ ์ ์ง์ ์๊ธฐ๋ก QA๋ฅผ ํ๋ ๋ ๋ค์ด ๋ง์๋๋ฐ, ์์ผ๋ก๋ ๋ฆฌํฉํฐ๋ง ํ ๋ ์ต๋ํ ํ ์คํธ ์ฝ๋์ ์์กดํ์ฌ ์์ ํ๋๋ก ์ต๊ด์ ๋ค์ฌ์ผ๊ฒ ๋ค. ๋จ์ถํค ์ฐ์ต๋ ์ด์ฌํ ํด์ ์์ผ๋ก๋ ํค๋ณด๋๋ก๋ง ๊ฐ๋ฐํ ์ ์๋๋ก... ๋ ธ๋ ฅํด์ผ๊ฒ ๋ค ๐ฅน