활동 내용

10진 계산기 타입 구현 및 리팩터 (개인)

  • Protocols
    • Resetable: 계산기 초기화 기능 (스택 배열 removeAll() 메서드로 구현)
    • Addable: 계산기 덧셈 기능(공통 기능) + 초기 구현
    • Subtractable: 계산기 뺄셈 기능(공통 기능) + 초기 구현
    • CalculationExecutable: 입력 연산자별 계산 기능 분류용 메서드 요구
    • TypeConvertible: 사용자가 입력한 숫자의 타입 변환 메서드 요구 (String? -> Double or String)
  • Calculators
    • DecimalCalculator: 10진 계산기 타입 (사칙연산, 연산자 우선순위 적용 계산 및 실시간 계산 결과 표현(연산자 클릭 시 연산자 우선순위 고려하여 즉시 계산 결과 표현))
    • BinaryCalculator: 이진 계산기 타입 (미구현)
  • Operator: 십진 및 이진 연산자 모음 열거 타입
  • Stack: 사용자 입력 숫자 및 연산자 저장용 스택 타입

향후 UI에서 숫자와 연산자를 입력 받으면 숫자의 stack 저장과 연산이 연산자를 누르는 시점에 일어나는 점에 착안하여 연산자를 탭했을 때를 대응하기 위한 메서드 executeCalculation(of:) 메서드를 만들었습니다.

mutating func executeCalculation(of _operator: Operator) {
    let userInput = inputAndConvertType()
    guard let previousOperator: Operator = operatingSequence.top else {
        stack.push(userInput)
        operatingSequence.push(_operator)
        return
    }
    
    switch _operator {
    case .addition:
        executeAddition(with: userInput, and: previousOperator)
    case .subtraction:
        executeSubtraction(with: userInput, and: previousOperator)
    case .multiplication:
        executeMultiplication(with: userInput, and: previousOperator)
    case .division:
        executeDivision(with: userInput, and: previousOperator)
    default:
        print("십진 계산에 적합하지 않은 연산자입니다.")
    }
}

고민한 점

팀 협의에 따라 계산기의 설계 목표를 맥과 아이폰의 계산기 애플리케이션으로 설정하였습니다. 실시간으로 계산을 진행한다는 컨셉을 유지하면 흔히 사용하는 후위표기법(postfix)을 사용하여 계산기 로직을 구현하기 어렵다는 단점이 있었기에 해당 로직을 구현하기 위해 더 많은 고민을 해야했습니다. 또한, 콘솔로그에서 readLine() 함수로 사용자의 입력을 받을 때는 enter가 입력이 완료되었다는 표현을 해주지만, 앱에서는 단순히 숫자 입력을 위한 버튼 터치가 입력이 완료되었음을 알리는 것이 아니기 때문에 (한 자리 숫자를 입력한다는 보장이 없으며, 실수 입력을 한다면 한 숫자를 표현하기 위해 여러 숫자를 탭해야함) 연산자를 누르는 시점에 스택 입력, 연산 작업과 숫자 스택, 연산자 스택 정리를 모두 처리하여야 했기에 메서드를 작성하는데 많은 어려움이 있었습니다.
이에 더해, 실시간으로 계산 결과를 보여준다는 컨셉은 향후 사용자가 입력하는 연산자가 먼저 입력한 연산자에 비해 우선 순위가 높을 수 있으며, 입력한 연산자가 단순히 본인의 연산을 진행한다는 보장이 없습니다 (예를 들어, 1 - 2 * 3 + 4를 수행한다면, +를 누르는 시점에 1 - 2 * 3을 수행해야 합니다. 반면에 +를 누르는 시점에 먼저 입력된 연산자 중 -가 없다면 먼저 나온 수들을 모두 더해주면 됩니다.). 이런 모든 시나리오에 대응하기 위해 로직이 어려워지는 문제가 있었습니다. 활동 내용의 예시코드에서 각 연산별 케이스 분기를 할 때 executeAddition(with:and:)와 같이 대응 메서드가 있다는 점을 보더라도 로직이 쉽지 않음을 예상할 수 있습니다. 예시를 위해 executeSubtraction(with:and:) 메서드와 이를 실행시키기 위한 서브 메서드들을 가져왔습니다.

private mutating func executeSubtraction(with userInput: Double, and previousOperator: Operator) {
    let poppedElement: Double = stack.pop() ?? 0
    
    if previousOperator == .addition {
        stack.push(add(poppedElement, and: userInput))
    } else if previousOperator == .subtraction {
        stack.push(subtract(poppedElement, and: userInput))
    } else if previousOperator == .multiplication {
        executeSubtractionWherePreviousOperatorIsMultiplication(with: userInput, and: poppedElement)
    } else if previousOperator == .division {
        executeSubtractionWherePreviousOperatorIsDivision(with: userInput, and: poppedElement)
    } else {
        print("이전 연산자가 없는데 여기까지 왔어요.. 무슨일이죠..? \(#function)")
    }
    operatingSequence.reset()
    operatingSequence.push(.subtraction)
}

private mutating func executeSubtractionWherePreviousOperatorIsMultiplication(with userInput: Double,
                                                                              and poppedElement: Double) {
    if operatingSequence.contains(.addition) {
        stack.push(multiply(poppedElement, and: userInput))
        let result = stack.sumAllElements()
        stack.reset()
        stack.push(result)
    } else {
        stack.push(multiply(poppedElement, and: userInput))
        let result = stack.subtractAllElements()
        stack.reset()
        stack.push(result)
    }
}

private mutating func executeSubtractionWherePreviousOperatorIsDivision(with userInput: Double,
                                                                        and poppedElement: Double) {
    if operatingSequence.contains(.addition) {
        stack.push(divide(poppedElement, and: userInput))
        let result = stack.sumAllElements()
        stack.reset()
        stack.push(result)
    } else {
        stack.push(divide(poppedElement, and: userInput))
        let result = stack.subtractAllElements()
        stack.reset()
        stack.push(result)
    }
}

위 코드를 통해 제 고군분투(?)를 어느정도 엿보실 수 있으실 것입니다. 단순히 콘솔로그를 통해 입력을 받는다고 하면 readLine() 메서드를 한 메서드 내에 두 번 배치(한 개의 메서드로 1 + 2 처럼 다음 입력할 대상을 미리 받음)하여 간결하게 작성이 가능할 듯 하나, 연산자 한 번 클릭에 사용자 숫자 입력 한 번 입력이라는 제한을 스스로 걸어두고 이렇게 작성했습니다. 아직 UI를 만들고 코드와 연결시켜본 경험이 부족해 이렇지만 앞으로 발전해 나가겠습니다~!

profile
합리적인 해법 찾기를 좋아합니다.

0개의 댓글