여러가지/이것저것

[OOP]사칙연산 계산기

hyeseong-dev 2024. 4. 4. 10:02

요구사항

  • 간단한 사칙연산을 할 수 있다.
  • 양수로만 계산 할 수 있다.
  • 나눗셈에서 0을 나누는 경우 적절한 예외(IllegalArgument, ZeroDivisionError)를 발생 시킬 수 있다.

절차적 코드

  • Enum 기반: ArithmeticOperator Enum 클래스를 사용하여 구현합니다. 각 연산을 열거형 값으로 나타내며, 각 값은 연산을 수행하는 calculate 메소드를 구현합니다.

자바

package org.example;

import java.util.stream.Stream;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.Arguments;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * 요구사항
 * 간단한 사칙연산을 할 수 있다.
 * 양수로만 계산할 수 있다.
 * 나눗셈에서 0을 나누는 경우 IllegalArgument 예외를 발생시킨다.
 * MVC패턴(Model-View-Controller) base로 구현한다.
 */
public class CalculatorTest {

    @DisplayName("덧셈 연산을 수행한다.")
    @ParameterizedTest
    @MethodSource("formulaAndResult")
    void additionTest(int operand1, String operator, int operand2, int result) {
        int calculateResult = Calculator.calculate(operand1, operator, operand2);

        assertThat(calculateResult).isEqualTo(result);
    }

    private static Stream<Arguments> formulaAndResult(){
        return Stream.of(
                Arguments.arguments(1, "+", 2, 3),
                Arguments.arguments(1, "-", 2, -1),
                Arguments.arguments(1, "*", 2, 2),
                Arguments.arguments(4, "/", 2, 2)
        );
    }
}
package org.example;

public class Calculator {

    public static int calculate(int operand1, String operator, int operand2) {
        switch (operator) {
            case "+":
                return add(operand1, operand2);
            case "-":
                return subtract(operand1, operand2);
            case "*":
                return multiply(operand1, operand2);
            case "/":
                return divide(operand1, operand2);
            default:
                throw new IllegalArgumentException("Invalid operator: " + operator);
        }
    }

    private static int add(int operand1, int operand2) {
        return operand1 + operand2;
    }

    private static int subtract(int operand1, int operand2) {
        return operand1 - operand2;
    }

    private static int multiply(int operand1, int operand2) {
        return operand1 * operand2;
    }

    private static int divide(int operand1, int operand2) {
        if (operand2 == 0) {
            throw new ArithmeticException("Cannot divide by zero.");
        }
        return operand1 / operand2;
    }
}

파이썬

def add(operand1, operand2):
    return operand1 + operand2

def subtract(operand1, operand2):
    return operand1 - operand2

def multiply(operand1, operand2):
    return operand1 * operand2

def divide(operand1, operand2):
    if operand2 == 0:
        raise ValueError("Cannot divide by zero.")
    return operand1 / operand2

def validate_positive_number(value):
    if value < 0:
        raise ValueError("Negative numbers are not allowed.")
    return value

def calculate(num1, operator, num2):
    operand1 = validate_positive_number(num1)
    operand2 = validate_positive_number(num2)

    if operator == "+":
        return add(operand1, operand2)
    elif operator == "-":
        return subtract(operand1, operand2)
    elif operator == "*":
        return multiply(operand1, operand2)
    elif operator == "/":
        return divide(operand1, operand2)
    else:
        raise ValueError("Invalid operator.")

if __name__ == '__main__':
    num1 = 10
    num2 = 4

    # 사칙연산 수행
    print(calculate(num1, "+", num2))  # 출력: 14
    print(calculate(num1, "-", num2))  # 출력: 6
    print(calculate(num1, "*", num2))  # 출력: 40
    print(calculate(num1, "/", num2))  # 출력: 2.5

객체지향 코드1

자바

package org.example.calculator.domain;

import java.util.Arrays;

public enum ArithmeticOperator {
    ADDITION("+") {
        @Override
        public int calculate(final int operand1, final int operand2) {
            return operand1 + operand2;
        }
    },
    SUBTRACTION("-") {
        @Override
        public int calculate(final int operand1, final int operand2) {
            return operand1 - operand2;
        }
    },
    MULTIPLICATION("*") {
        @Override
        public int calculate(final int operand1, final int operand2) {
            return operand1 * operand2;
        }
    }, DIVISION("/") {
        @Override
        public int calculate(final int operand1, final int operand2) {
            if (operand2 == 0) {
                throw new IllegalArgumentException("0으로 나눌 수 없습니다.");
            }
            return operand1 / operand2;
        }
    };

    private final String operator;

    ArithmeticOperator(String operator) {
        this.operator = operator;
    }

    public abstract int calculate(final int operand1, final int operand2);

    public static int calculate(final int operand1, final String operator, final int operand2) {
        ArithmeticOperator selectedArithmeticOperator = Arrays.stream(ArithmeticOperator.values())
                .filter(v -> v.operator.equals(operator))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("올바른 사칙연산이 아닙니다."));

        return selectedArithmeticOperator.calculate(operand1, operand2);
    }
}
package org.example.calculator.domain;

public class PositiveNumber {
    private static final String NEGATIVE_NUMBER_EXCEPTION_MESSAGE = "음수를 전달할 수 없습니다.";

    private final int value;

    public PositiveNumber(int value) {
        validate(value);
        this.value = value;
    }

    private void validate(int value) {
        if (isNegativeNumber(value)) {
            throw new IllegalArgumentException(NEGATIVE_NUMBER_EXCEPTION_MESSAGE);
        }
    }

    private boolean isNegativeNumber(int number) {
        return number < 0;
    }

    public int toInt() {
        return value;
    }
}
package org.example.calculator.domain;

public class Calculator {
    public static int calculate(PositiveNumber num1, String operator, PositiveNumber num2) {
        return ArithmeticOperator.calculate(num1.toInt(), operator, num2.toInt());
    }
}

파이썬

calculator1패키지를 만듭니다.

__init__.py 모듈을 작성합니다.

# calculator1/__init__.py

Number = int | float

basic_arithmetic모듈을 작성합니다.

# calculator1/basic_arithmetic.py

from operation_strategy import OperationStrategy
from calculator1 import Number

class BasicArithmeticOperation(OperationStrategy):

    @staticmethod
    def add(operand1: Number, operand2: Number) -> Number:
        return operand1 + operand2

    @staticmethod
    def subtract(operand1: Number, operand2: Number) -> Number:
        return operand1 - operand2

    @staticmethod
    def multiply(operand1: Number, operand2: Number) -> Number:
        return operand1 * operand2

    @staticmethod
    def divide(operand1: Number, operand2: Number) -> Number:
        return operand1 / operand2

operation_strategy 모듈을 작성합니다.

# calculator1/operation_strategy.py

from abc import ABC, abstractmethod
from calculator1 import Number
class OperationStrategy(ABC):

    @staticmethod
    @abstractmethod
    def add(operand1: Number, operand2: Number) -> Number:
        pass

    @staticmethod
    @abstractmethod
    def subtract(operand1: Number, operand2: Number) -> Number:
        pass

    @staticmethod
    @abstractmethod
    def multiply(operand1: Number, operand2: Number) -> Number:
        pass

    @staticmethod
    @abstractmethod
    def divide(operand1: Number, operand2: Number) -> Number:
        pass

    @classmethod
    def execute_operation(cls, operator: str, operand1: Number, operand2: Number) -> Number:
        operations = {
            "+": cls.add,
            "-": cls.subtract,
            "*": cls.multiply,
            "/": cls.divide,
        }
        if operator in operations:
            return operations[operator](operand1, operand2)
        else:
            raise ValueError("Invalid operator")

PositiveNumber모듈을 작성합니다.

# calculator1/positive_number.py

from calculator1 import Number
class PositiveNumber:
    def __init__(self, value: Number):
        self.__validate(value)
        self.value = value

    @staticmethod
    def __validate(value: Number) -> None:
        if value < 0:
            raise ValueError("Negative numbers are not allowed.")

    def to_int(self) -> Number:
        return self.value

calculator모듈을 작성합니다.

# calculator1/calculator.py

from positive_number import PositiveNumber
from basic_arithmetic_operation import BasicArithmeticOperation

class Calculator:
    @staticmethod
    def calculate(num1: PositiveNumber, operator: str, num2: PositiveNumber) -> int:
        operand1 = num1.to_int()
        operand2 = num2.to_int()
        result: int = BasicArithmeticOperation.execute_operation(operator, operand1, operand2)
        return result


if __name__ == '__main__':
    num1 = PositiveNumber(10)
    num2 = PositiveNumber(4)

    # 사칙연산 수행
    print(Calculator.calculate(num1, "+", num2))  # 15
    print(Calculator.calculate(num1, "-", num2))  # 5
    print(Calculator.calculate(num1, "*", num2))  # 50
    print(Calculator.calculate(num1, "/", num2))  # 2.0

객체지향 코드2

계산기 주요 기능을 제공하는 클래스입니다.
이 클래스는 사칙 연산을 수행하는 정적 메서드 calculate를 포함하고 있습니다.

package org.example;

import org.example.calculator.*;

import java.util.List;

public class Calculator {

    private static final List<NewArithmeticOperator> arithmeticOperators = List.of(
            new AdditionOperator(),
            new SubtractOperator(),
            new MultiplyOperator(),
            new DivideOperator()
    );

    public static int calculate(int operand1, String operator, int operand2){
        return arithmeticOperators.stream()
                .filter(arithmeticOperator -> arithmeticOperator.support(operator))
                .map(arithmeticOperator -> arithmeticOperator.calculate(operand1, operand2))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("올바른 사칙연산이 아닙니다"));
    }
}

사칙연산을 테스트하는 클래스입니다.
이 클래스는 JUnit을 사용하여 Calculator 클래스의 calculate 메서드를 테스트합니다.
테스트는 다양한 사칙연산에 대해 수행됩니다.

package org.example;

import java.util.stream.Stream;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.Arguments;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * 요구사항
 * 간단한 사칙연산을 할 수 있다.
 * 양수로만 계산할 수 있다.
 * 나눗셈에서 0을 나누는 경우 IllegalArgument 예외를 발생시킨다.
 * MVC패턴(Model-View-Controller) base로 구현한다.
 */
public class CalculatorTest {

    @DisplayName("사칙연산 테스트를 수행한다.")
    @ParameterizedTest
    @MethodSource("formulaAndResult")
    void calculateTest(int operand1, String operator, int operand2, int result) {
        int calculateResult = Calculator.calculate(operand1, operator, operand2);

        assertThat(calculateResult).isEqualTo(result);
    }

    private static Stream<Arguments> formulaAndResult(){
        return Stream.of(
                Arguments.arguments(1, "+", 2, 3),
                Arguments.arguments(1, "-", 2, -1),
                Arguments.arguments(1, "*", 2, 2),
                Arguments.arguments(4, "/", 2, 2)
        );
    }
}

양수를 나타내는 클래스입니다.
이 클래스는 생성 시점에 전달된 값이 양수인지 검증합니다.
양수가 아닐 경우 IllegalArgumentException을 발생시킵니다.

package org.example.calculator.domain;

public class PositiveNumber {
    public static final String ZERO_OR_NEGATIVE_NUMBER_EXCEPTION_MESSAGE = "0 또는 음수를 전달할 수 없습니다.";

    private final int value;

    public PositiveNumber(int value) {
        validate(value);
        this.value = value;
    }

    private void validate(int value) {
        if (isNegativeNumber(value)) {
            throw new IllegalArgumentException(ZERO_OR_NEGATIVE_NUMBER_EXCEPTION_MESSAGE);
        }
    }

    private boolean isNegativeNumber(int number) {
        return number <= 0;
    }

    public int toInt() {
        return value;
    }
}

사칙 연산을 지원하는 연산자 인터페이스입니다.
support 메서드를 통해 해당 연산자가 지원하는지 확인할 수 있습니다.
calculate 메서드는 두 피연산자에 대한 계산을 수행합니다.

package org.example.calculator;

public interface NewArithmeticOperator {
    boolean support(String operator);
    int calculate(int operand1, int operand2);
}

덧셈 연산을 수행하는 클래스입니다.
NewArithmeticOperator 인터페이스를 구현합니다.

package org.example.calculator;

public class AdditionOperator implements NewArithmeticOperator{
    @Override
    public boolean support(String operator) {
        return "+".equals(operator);
    }

    @Override
    public int calculate(int operand1, int operand2) {
        return operand1 + operand2;
    }
}

뺄셈 연산을 수행하는 클래스입니다.
NewArithmeticOperator 인터페이스를 구현합니다.

package org.example.calculator;

public class SubtractOperator implements NewArithmeticOperator{
    @Override
    public boolean support(String operator) {
        return "-".equals(operator);
    }

    @Override
    public int calculate(int operand1, int operand2) {
        return operand1 - operand2;
    }
}

곱셈 연산을 수행하는 클래스입니다.
NewArithmeticOperator 인터페이스를 구현합니다.

package org.example.calculator;

public class MultiplyOperator implements NewArithmeticOperator{
    @Override
    public boolean support(String operator) {
        return "*".equals(operator);
    }

    @Override
    public int calculate(int operand1, int operand2) {
        return operand1 * operand2;
    }
}

나눗셈 연산을 수행하는 클래스입니다.
NewArithmeticOperator 인터페이스를 구현합니다.
0으로 나누는 경우를 처리하기 위해 추가적인 검증이 필요할 수 있습니다.

package org.example.calculator;

public class DivideOperator implements NewArithmeticOperator{
    @Override
    public boolean support(String operator) {
        return "/".equals(operator);
    }

    @Override
    public int calculate(int operand1, int operand2) {
        return operand1 / operand2;
    }
}

파이썬

from typing import Generic, TypeVar, List
from abc import ABC, abstractmethod

# 숫자 타입 변수 정의 (정수 또는 부동소수점)
NumberType = TypeVar('NumberType', int, float)

class NewArithmeticOperator(ABC, Generic[NumberType]):
    @abstractmethod
    def support(self, operator: str) -> bool:
        pass

    @abstractmethod
    def calculate(self, operand1: NumberType, operand2: NumberType) -> NumberType:
        pass

class AdditionOperator(NewArithmeticOperator[NumberType]):
    def support(self, operator: str) -> bool:
        return operator == "+"

    def calculate(self, operand1: NumberType, operand2: NumberType) -> NumberType:
        return operand1 + operand2

class SubtractOperator(NewArithmeticOperator[NumberType]):
    def support(self, operator: str) -> bool:
        return operator == "-"

    def calculate(self, operand1: NumberType, operand2: NumberType) -> NumberType:
        return operand1 - operand2

class MultiplyOperator(NewArithmeticOperator[NumberType]):
    def support(self, operator: str) -> bool:
        return operator == "*"

    def calculate(self, operand1: NumberType, operand2: NumberType) -> NumberType:
        return operand1 * operand2

class DivideOperator(NewArithmeticOperator[NumberType]):
    def support(self, operator: str) -> bool:
        return operator == "/"

    def calculate(self, operand1: NumberType, operand2: NumberType) -> NumberType:
        if operand2 == 0:
            raise ValueError("Cannot divide by zero.")
        return operand1 / operand2

class Calculator:
    arithmetic_operators: List[NewArithmeticOperator[NumberType]] = [
        AdditionOperator(),
        SubtractOperator(),
        MultiplyOperator(),
        DivideOperator()
    ]

    @staticmethod
    def calculate(operand1: NumberType, operator: str, operand2: NumberType) -> NumberType:
        for arithmetic_operator in Calculator.arithmetic_operators:
            if arithmetic_operator.support(operator):
                return arithmetic_operator.calculate(operand1, operand2)
        raise ValueError("올바른 사칙연산이 아닙니다")

class PositiveNumber(Generic[NumberType]):
    ZERO_OR_NEGATIVE_NUMBER_EXCEPTION_MESSAGE = "0 또는 음수를 전달할 수 없습니다."

    def __init__(self, value: NumberType):
        self.validate(value)
        self.value: NumberType = value

    def validate(self, value: NumberType) -> None:
        if value <= 0:
            raise ValueError(self.ZERO_OR_NEGATIVE_NUMBER_EXCEPTION_MESSAGE)

    def to_number(self) -> NumberType:
        return self.value

if __name__ == '__main__':
    num1 = PositiveNumber(10).to_number()
    num2 = PositiveNumber(4).to_number()

    # 사칙연산 수행
    print(Calculator.calculate(num1, "+", num2))  # 14
    print(Calculator.calculate(num1, "-", num2))  # 6
    print(Calculator.calculate(num1, "*", num2))  # 40
    print(Calculator.calculate(num1, "/", num2))  # 2.5

코드 설명

위 파이썬 코드는 사칙 연산을 수행하는 계산기의 기능을 구현한 것으로, 여러 객체지향 프로그래밍(OOP)과 제네릭 프로그래밍의 특징을 보여줍니다. 주요 구성 요소는 다음과 같습니다:

NumberType 제네릭 타입 변수

NumberType은 정수(int) 또는 부동소수점(float) 타입을 나타내는 타입 변수입니다. 이를 사용하여 다양한 숫자 타입을 처리할 수 있는 유연성을 제공합니다.

NewArithmeticOperator 추상 베이스 클래스

NewArithmeticOperator는 사칙 연산을 나타내는 추상 베이스 클래스(ABC)로, 제네릭 타입 NumberType을 사용합니다. 이 클래스는 두 가지 추상 메서드를 정의합니다:

  • support(self, operator: str) -> bool: 주어진 연산자 문자열을 지원하는지 여부를 반환합니다.
  • calculate(self, operand1: NumberType, operand2: NumberType) -> NumberType: 두 피연산자에 대한 계산을 수행하고 결과를 반환합니다.

사칙 연산 클래스들

AdditionOperator, SubtractOperator, MultiplyOperator, DivideOperator 클래스들은 NewArithmeticOperator 추상 클래스를 구현합니다. 각 클래스는 특정 사칙 연산을 나타내며, support 메서드를 통해 해당 연산을 처리할 수 있는지를 판단하고, calculate 메서드로 실제 연산을 수행합니다.

Calculator 클래스

Calculator 클래스는 사칙 연산을 수행하는 정적 메서드 calculate를 포함합니다. 이 메서드는 피연산자와 연산자를 인자로 받아, 등록된 사칙 연산 클래스들 중 해당 연산을 처리할 수 있는 클래스를 찾아 연산을 수행합니다. 연산자를 처리할 수 있는 클래스가 없을 경우, ValueError를 발생시킵니다.

PositiveNumber 클래스

PositiveNumber 클래스는 양수만을 허용하는 래퍼 클래스로, 제네릭 타입 NumberType을 사용합니다. 생성자에서 주어진 값이 양수인지 검증하며, 그렇지 않을 경우 ValueError를 발생시킵니다.

메인 실행 부

if __name__ == '__main__': 부분에서는 PositiveNumber 클래스를 사용하여 사칙 연산을 수행하는 예를 보여줍니다. PositiveNumber 인스턴스에서 to_number 메서드를 사용하여 실제 숫자 값을 얻고, 이를 Calculator.calculate 메서드에 전달하여 사칙 연산의 결과를 출력합니다.

이 코드는 OOP의 원칙을 따르며, 특히 다형성과 캡슐화를 활용하여 사칙 연산 기능을 유연하고 확장 가능한 방식으로 구현합니다. 또한, 제네릭을 사용하여 타입 안전성을 높이면서도 다양한 숫자 타입을 처리할 수 있는 유연성을 제공합니다.