해당과정은 계산기 코드를 짜면서 단일 책임 원칙과 의존성 역전 원칙을 적용시켜 가면서 얻은 고찰입니다.
기존 코드를 짰을 때는 돌아기만 하면 된다는 생각으로 코드를 작성하였습니다.
기본적인 기능 구현
Calculator.kt
class Calculator {
private val keys = setOf(
Key.CALCULATE_STOP,
Key.CALCULATE_ADD,
Key.CALCULATE_MINUS,
Key.CALCULATE_MULTIPLY,
Key.CALCULATE_DEVIDE,
)
fun calculate() {
println("계산을 진행하겠습니다.")
println(
"계산의 종류\n" +
"-----------------------------------------------------------\n" +
"0 : 종료 | 1 : 더하기 | 2 : 빼기 | 3 : 곱하기 | 4 : 나누기\n" +
"-----------------------------------------------------------"
)
// 계산 종류 확인용
val key: Int
println("[계산의 종류]를 입력해주세요")
while (true) {
val line = readLine()!!
if (line.toIntOrNull() != null && line.toInt() in keys) {
key = line.toInt()
break
} else {
println("값이 맞지 않습니다! 값을 확인해주세요!")
}
}
if (key == Key.CALCULATE_STOP) {
println("계산을 종료하겠습니다.")
return
}
val numA: Double
val numB: Double
// 계산 인자 확인용
println("[인자A] [인자B] 위 양식대로 값을 입력해주세요")
while (true) {
val readList = readLine()!!.split(" ")
if (readList.size != 2) {
println("양식대로 값을 넣어주세요")
continue
}
val numAIsCurrect = readList[0].toDoubleOrNull() != null
val numBIsCurrect =
readList[1].toDoubleOrNull() != null && (key != Key.CALCULATE_DEVIDE || readList[1].toDouble() != 0.0)
if (numAIsCurrect && numBIsCurrect) {
numA = readList[0].toDouble()
numB = readList[1].toDouble()
break
}
if (!numAIsCurrect) println("인자A값을 확인해주세요")
if (!numBIsCurrect) println("인자B값을 확인해주세요, 나누기의 경우 인자B의경우 0을 넣으실수 없습니다")
}
when (key) {
Key.CALCULATE_ADD -> println("계산결과 : ${add(numA, numB)}")
Key.CALCULATE_MINUS -> println("계산결과 : ${minus(numA, numB)}")
Key.CALCULATE_MULTIPLY -> println("계산결과 : ${multiply(numA, numB)}")
Key.CALCULATE_DEVIDE -> println("계산결과 : ${devide(numA, numB)}")
}
println("계산을 계속 하시겠습니까?[y,n]")
if (readLine() == "y") calculate() else {
println("계산을 종료하겠습니다.")
return
}
}
private fun add(a: Double, b: Double) = a + b
private fun minus(a: Double, b: Double) = a - b
private fun multiply(a: Double, b: Double) = a * b
private fun devide(a: Double, b: Double): Double = a / b
}
Main.kt
fun main() {
val calculator : Calculator = Calculator()
calculator.calculate()
}
Key.kt
object Key {
const val CALCULATE_STOP = 0
const val CALCULATE_ADD = 1
const val CALCULATE_MINUS = 2
const val CALCULATE_MULTIPLY = 3
const val CALCULATE_DEVIDE = 4
}
이후 단일 책임 원칙과 의존성 역전 원칙을 지키기 위해 각 책임들을 나눠 클래스로 보내고 비슷한 부분들을 나누려고 시도하였습니다. 의존성 역전원칙은 그저 계산 부분을 abstract클래스로 새로 만들어 해당동작들에 적용하면 되었지만 문제는 단일 책임원칙이었습니다. 기존 Calculator 쪽 계산 부분은 단일 책임원칙으로 각 클래스를 넣어주고 말았지만 유형성 검사 부분이나 다른 출력문 또는 재귀적인 부분에서는 어떻게 나눠야 할지 감이 안 왔기 때문에 많은 고민을 하였습니다.
그래서 초안으로 해당 Calculator클래스를 전채적인 동작을 관리하는 클래스로 만들고 재귀는 반복문을 이용하여 가독성을 높이고 출력들의 경우 유효성 검사 부분은 따로 빼서 클래스로 만들어 Triple객채를 반환하는 방식으로 바꾸었습니다. 나머지 부분은 따로 뺄 방법이 생각나지 않아 그대로 사용하였습니다.
단일 책임 원칙 의존성 역전 원칙 적용 #1
Calculator.kt
class Calculator {
fun calculate() {
while (true) {
//값에대한 유효성 검사
val (key, numA, numB) = ShowRule().getArguments()
if (key == Key.CALCULATE_STOP) break
val operator: AbstractOperation = when (key) {
Key.CALCULATE_ADD -> AddOperation()
Key.CALCULATE_SUBTRACT -> SubtractOperation()
Key.CALCULATE_MULTIPLY -> MultiplyOperation()
Key.CALCULATE_DIVIDE -> DivideOperation()
else -> return
}
println("계산결과 : ${operator.operate(numA, numB)}")
println("계산을 계속 하시겠습니까?[y]")
if (readLine() != "y") break
}
println("계산을 종료하겠습니다.")
}
}
AbstractOperation.kt
abstract class AbstractOperation {
abstract fun operate(a:Double,b:Double) : Double
}
AddOperation.kt
class AddOperation : AbstractOperation() {
override fun operate(a: Double, b: Double) = a + b
}
SubtractOperation과 MultiplyOperation, DivideOperation은 위와 비슷하게 AbstractOperation()을 상속받으면 operate안에 각동작을 넣음.
ShowRule.kt
class ShowRule {
fun getArguments(): Triple<Int, Double, Double> {
println("계산을 진행하겠습니다.")
println(
"계산의 종류\n" +
"------------------------------------------------------------------------\n" +
"0 : 종료 | 1 : 더하기 | 2 : 빼기 | 3 : 곱하기 | 4 : 나누기\n" +
"------------------------------------------------------------------------"
)
// 계산 종류 확인용
val key: Int
println("[계산의 종류]를 입력해주세요")
while (true) {
val line = readLine()!!
if (line.toIntOrNull() != null && line.toInt() in Key.keys) {
key = line.toInt()
break
} else {
println("값이 맞지 않습니다! 값을 확인해주세요!")
}
}
if (key == Key.CALCULATE_STOP) {
return Triple(Key.CALCULATE_STOP, 0.0, 0.0)
}
val numA: Double
val numB: Double
// 계산 인자 확인용
println("[인자A] [인자B] 위 양식대로 값을 입력해주세요")
while (true) {
val readList = readLine()!!.split(" ")
if (readList.size != 2) {
println("양식대로 값을 넣어주세요")
continue
}
val numAIsCurrect = readList[0].toDoubleOrNull() != null
val numBIsCurrect =
readList[1].toDoubleOrNull() != null && ((key != Key.CALCULATE_DIVIDE) || readList[1].toDouble() != 0.0)
if (numAIsCurrect && numBIsCurrect) {
numA = readList[0].toDouble()
numB = readList[1].toDouble()
break
}
if (!numAIsCurrect) println("인자A값을 확인해주세요")
if (!numBIsCurrect) println("인자B값을 확인해주세요")
}
return Triple(key, numA, numB)
}
}
Key.kt
object Key {
const val CALCULATE_STOP = 0
const val CALCULATE_ADD = 1
const val CALCULATE_SUBTRACT = 2
const val CALCULATE_MULTIPLY = 3
const val CALCULATE_DIVIDE = 4
val keys = setOf(
CALCULATE_STOP,
CALCULATE_ADD,
CALCULATE_SUBTRACT,
CALCULATE_MULTIPLY,
CALCULATE_DIVIDE,
)
}
이렇게 구성하고도 Calculator 쪽이 너무 많은 책임을 가지고 있다고 생각되었습니다. 아무리 Calculator가 모든 계산을 담당하더라도 너무 많은 기능을 담고 있어 Calculator가 과연 단일책임원칙을 지키고 있는 것인지 의문이 들었습니다 그래서 해당 Calculator가 계산을 진행하는 부분을 남겨두되 계산결과만 반환하도록 수정하였고 main함수에서 전채적인 계산을 관리하도록 수정하였습니다. 또한 각각의 책임을 다루는 원칙으로 수정하였을 때 기존에는 calculate안에서만 동작하였기 때문에 계산키를 넘치게 입력한다든가 나누기를 할 때 나누는 값을 0으로 했을 때 오류가 생길 부분은 유효성 검사 부분에서 막아놓았기 때문에 해당 오류를 방지할 수 있었습니다. 그렇지만 해당 책임들을 함수로 바꾸니 해당함수가 어디에서 호출될지 모르기 때문에 해당연산으로 인한 예상치 못한 동작들을 막아줄 필요가 있었습니다.
단일 책임 원칙 의존성 역전 원칙 적용 #2
수정된 부분
Calculator.kt
class Calculator {
fun calculate(key: Int, numA: Double, numB: Double): String {
val operator: AbstractOperation = when (key) {
Key.CALCULATE_ADD -> AddOperation()
Key.CALCULATE_SUBTRACT -> SubtractOperation()
Key.CALCULATE_MULTIPLY -> MultiplyOperation()
Key.CALCULATE_DIVIDE -> DivideOperation()
else -> throw IllegalArgumentException("key($key) is over range")
}
return "계산결과 : ${operator.operate(numA, numB)}"
}
}
DivideOperation.kt
class DivideOperation : AbstractOperation() {
override fun operate(a: Double, b: Double): Double {
require(b != 0.0) { "b is zero. So it can't operate" }
return a / b
}
}
Main.kt
fun main() {
val calculator: Calculator = Calculator()
val rule: ShowRule = ShowRule()
while (true) {
//값에대한 유효성 검사
val (key, numA, numB) = rule.getArguments()
if (key == Key.CALCULATE_STOP) break
try {
val result = calculator.calculate(key, numA, numB)
println(result)
} catch (illegalArgumentException: IllegalArgumentException) {
println(illegalArgumentException)
continue
}
println("계산을 계속 하시겠습니까?[y]")
if (readLine() != "y") break
}
println("계산을 종료하겠습니다.")
}
단일 책임 원칙을 수행하며 느낀 점은 기존의 코드를 수정할 때보다 유지보수가 쉽다는 점입니다. 저는 이 과정들을 수행 하면서 코드들을 유지보수하는 작업을 계속하였습니다. 그러던 중 분명 오류가 있었던 부분도 있었고 수정할 부분이 생각날 때도 있었습니다. 이러한 유지보수는 처음 코드의 기능동작을 구현했을 때나 위원칙을 지킨 후에도 있었습니다. 전자에서는 해당 부분의 코드들을 찾기 위해 전채 코드들을 봐야 했습니다. 후자의 경우 책임별로 해당 동작들의 나누었기 때문에 해당동작이 일어나는 부분만 보면 됐습니다. 이점은 큰 프로젝트를 하게 된다면 더욱 단일 책임 원칙의 필요성을 느낄 것입니다. 큰 프로젝트가 만약 하나의 파일로 구현돼 있다면 오류가 발생했을 때 해당 구역을 찾기 위해 많은 시간과 노동이 들어갈 것입니다. 그렇지만 단일 책임 원칙을 잘 지킨 코드의 경우 해당기능이 수행이 된 파일만 보면 되기 때문 시간을 아낄 수 있을 것입니다.
또한 해당 책임별로 클래스를 나눌경우 협업을 할 때에도 팀원을 나눌 때 파트별로 나누기 더 쉬워질 것입니다.
의존성 역전 원칙을 수행하며 느낀 점은 코드의 통일성이 있다는 점입니다. 기존의 코드 같은 경우에도 프로젝트가 크지 않아 통일성을 어긴 부분은 잘 느껴지지 않지만 만약 단일 책임원칙을 이용하여 구현된 비슷한 로직들을 호출한다고 생각해 봅시다 이경우 각 연산의 함수들의 이름이 다르다면 해당함수를 호출할 때마다 이 함수가 맞나 해당 로직들을 봐야 할 것입니다. 그렇지만 의존성 역전 원칙으로 잘 수행된 로직이라면 위 #1,2의 코드들처럼 AbstractOperation으로 넣어준다면 해당로직들로 구현된 다른 클래스들을 집어넣을 수도 있으며 함수의 이름이 같기 때문에 쉽게 더욱 쉽게 통일성 있는 함수들을 이용할 수 있을 것입니다.
github : https://github.com/PotatoMeme/Calculator