계산기 프로그램 만들기

유건우·2024년 9월 4일

프로젝트

목록 보기
1/9

전체적인 다이어그램







적용된 아키텍쳐 패턴 - 팩토리 메서드 패턴


팩토리 메서드 패턴에 대해

  • 펙토리 메서드 패턴은 객체 생성의 책임을 서브클래스에게 위임하여 객체의 생성 방식을 변경할 수 있는 디자인 패턴입니다.
  • 객체 생성의 구체적인 클래스를 정의하지 않고 인터페이스추상 클래스를 통해 객체를 생성하는 방법을 정의합니다.




주요 개념

  • Product
    • 팩토리 메서드 패턴에서 생성되는 객체의 공통 인터페이스를 정의합니다.
    • 프로젝트에서는 AbstractOperation 이 해당됩니다.
  • Concrete Product
    • 제품 인터페이스를 구현한 구체적인 클래스들입니다.
    • 프로젝트에서는 AddIntegerOperation, AddDoubleOperation 등이 해당됩니다.
  • Factory
    • 객체 생성을 위한 메서드를 정의하는 인터페이스 또는 추상클래스입니다.
  • Concreate Factory
    • 팩토리 인터페이스를 구현하여 구체적인 제품을 생성하는 클래스입니다.
    • 이 클래스는 특정 제품 객체를 생성하는 방법을 제공합니다.
    • 프로젝트에서는 CalculatorIntegerFactory , CalculatorDoubleFactory 해당됩니다.




구조

+---------------------+
|   Product           |
+---------------------+
| +operation()        |
+---------------------+
           ^
           |
+---------------------+
|   ConcreteProductA  |
+---------------------+
| +operation()        |
+---------------------+
           ^
           |
+---------------------+
|   ConcreteProductB  |
+---------------------+
| +operation()        |
+---------------------+

+---------------------+
|   Creator           |
+---------------------+
| +factoryMethod()   |
+---------------------+
           ^
           |
+---------------------+
|   ConcreteCreatorA  |
+---------------------+
| +factoryMethod()   |
+---------------------+
           ^
           |
+---------------------+
|   ConcreteCreatorB  |
+---------------------+
| +factoryMethod()   |
+---------------------+




장점

  • 객체 생성로직을 팩토리 메서드에서 캡슐화하여, 클라이언트 코드가 객체 생성 방식에 신경 쓸 필요가 없습니다.
  • 새로운 제품이 추가되더라도 클라이언트 코드변경 없이 팩토리 메서드수정하면 됩니다.
  • 클라이언트 코드가 구체적인 클래스의존하지 않고, 인터페이스추상 클래스에 의존하기 때문에 클래스간의 결합도가 낮아집니다.





구현 코드


AbstractOperation - interface

public interface AbstractOperation<T extends Number> {

    T operator(T firstNumber, T secondNumber);
}
  • double, int 어떤 타입이 들어올지 모르기때문에 제네릭 타입으로 메소드를 정의해줍니다.




AddIntegerOperation - class

public class AddIntegerOperation implements AbstractOperation<Integer> {
    @Override
    public Integer operator(Integer firstNumber, Integer secondNumber) {
        return firstNumber + secondNumber;
    }
}
  • AbstactOperation<Integer> 인터페이스를 상속받아 구현한 구현체입니다.
  • Interger 타입으로 메소드를 수행하기 때문에 제네릭 타입명시해줍니다.





AddDoubleOperation - class

public class AddDoubleOperation implements AbstractOperation<Double> {

    @Override
    public Double operator(Double firstNumber, Double secondNumber) {
        return firstNumber + secondNumber;
    }
}
  • AbstactOperation<Double> 인터페이스를 상속받아 구현한 구현체입니다.

  • Double 타입으로 메소드를 수행하기 때문에 제네릭 타입명시해줍니다.

  • 각 연산자에 맞춰 나머지 연산자에 대한 구현도 해줍니다.





CalculatorIntegerFactory - enum

public enum CalculatorIntegerFactory {

    ADDINTEGER("+", new AddIntegerOperation()),
    SUBINTEGER("-", new SubIntegerOperation()),
    MULINTEGER("*", new MulIntegerOperation()),
    DIVINTEGER("/", new DivIntegerOperation());

    private final String operator;
    private final AbstractOperation<Integer> abstractOperation;

    CalculatorIntegerFactory(String operator, AbstractOperation<Integer> abstractOperation) {
        this.operator = operator;
        this.abstractOperation = abstractOperation;
    }

    public static AbstractOperation<Integer> operation(String operator) throws Exception {
        for (CalculatorIntegerFactory value : values()) {
            if(value.operator.equals(operator)) return value.abstractOperation;
        }
        return null;
    }
}
  • Factoryenum 클래스로 구현합니다.
  • 생성자통해 연산자, AbstactOperation 을 주입받습니다.
  • operation 메서드를 통해 연산자와 일치하는 enum 상수를 찾습니다.
  • 일치하는 enum 상수가 있으면 그에 해당하는 AbstractOperation<Integer> 객체를 반환합니다.
  • Double 타입도 동일하게 enum 클래스로 구현합니다.





ExceptionHandler - class

public class ExceptionHandler {

    private final String firstNum;
    private final String secondNum;
    private final String operator;
    private static final String OPERATION_REG = "[+\\-*/]";

    public ExceptionHandler(String firstNum, String secondNum, String operator) {
        this.firstNum = firstNum;
        this.secondNum = secondNum;
        this.operator = operator;
    }

    public void validation() throws Exception {
        boolean firstType = new TypeValidation().type(firstNum);
        boolean secondType = new TypeValidation().type(secondNum);

        if (!Pattern.matches(OPERATION_REG, this.operator)) throw new Exception("연산자 타입이 맞지 않습니다.");
        if ((firstType && !secondType) || (!firstType && secondType)) throw new Exception("숫자 타입이 서로 맞지 않습니다.");
        if (this.operator.equals("/") && secondType && Double.parseDouble(secondNum) == 0.0)
            throw new Exception("0으로 나눌 수 없습니다.");
        if (this.operator.equals("/") && !secondType && Integer.parseInt(secondNum) == 0)
            throw new Exception("0으로 나눌 수 없습니다.");
    }

}
  • 각 상황에 맞는 에러 처리하기 위한 클래스입니다.
  • if (!Pattern.matches(OPERATION_REG, this.operator))
    • 연산자 타입이 맞이 않으면 에러를 발생시킵니다.
  • if ((firstType && !secondType) || (!firstType && secondType))
    • 첫번째 수와 두번째 수가 서로 타입이 안맞을 경우 예외를 발생시킵니다.
  • if (this.operator.equals("/") && secondType && Double.parseDouble(secondNum) == 0.0)
    • 나눗셈 연산을 수행할 때 두번째 수가 0.0 일경우 예외를 발생시킵니다.
  • if (this.operator.equals("/") && !secondType && Integer.parseInt(secondNum) == 0)
    • 나눗셈 연산을 수행할 때 두번째 수가 0 일 경우 얘외를 발생시킵니다.





ArithmeticCalculator - class

public class ArithmeticCalculator {

    private final List<Number> result = new ArrayList<>();

    public Number calculation(String firstNumber, String secondNumber, String operator) throws Exception {

        new ExceptionHandler(firstNumber, secondNumber, operator).validation();
        boolean isDoubleType = new TypeValidation().type(firstNumber);

        if (isDoubleType) {
            AbstractOperation<Double> doubleAbstractOperation = CalculatorDoubleFactory.operation(operator);
            Double answer = doubleAbstractOperation.operator(Double.parseDouble(firstNumber), Double.parseDouble(secondNumber));
            result.add(answer);
            return answer;
        }

        AbstractOperation<Integer> integerOperation = CalculatorIntegerFactory.operation(operator);
        Integer answer = integerOperation.operator(Integer.parseInt(firstNumber), Integer.parseInt(secondNumber));
        result.add(answer);
        return answer;
    }

    public void getResult() {
        AtomicInteger idx = new AtomicInteger(0);
        result.forEach(number -> {
            int cnt = idx.incrementAndGet();
            System.out.println(cnt + "번째 연산결과 :" + number);
        });
    }
  • 펙토리 메서드를 통해 객체를 생성하고 List 에 결과를 저장하는 로직이 담겨있는 클래스입니다.
  • new ExceptionHandler() 를 통해 타입 검증을 수행합니다.
  • 이후 각 타입에 맞춰 AbstractOperation<T> 타입을 생성해줍니다.
  • 연산을 수행한 결과값은 List에 저장해줍니다.
  • getResult() 메소드는 lamda를 통해 저장한 연산결과를 출력해주는 메서드입니다.








📖 톺아보기

  • 팩토리 메서드 패턴을 통해 객체 생성 방식을 선택할 수 있었습니다.
  • 팩토리 메서드 패턴을 통해 객체 생성의 책임을 팩토리 메서드에 위임함으로써 객체 생성 로직과 비즈니스 로직을 분리할 수 있었습니다. 이는 클래스가 단일 책임을 가지게 하고, 객체 생성과 관련된 코드가 변경되더라도 비즈니스 로직에는 영향을 미치지 않도록 합니다. - 단일 책임 원칙 (SRP)
  • 새로운 Product를 추가할 때 기존의 코드나 클래스는 수정하지 않고 새로운 팩토리 메서드를 추가하거나 기존의 팩토리 클래스를 확장함으로써 새 객체를 생성할 수 있었습니다. - 개방/페쇄 원칙 (OCP)
  • 팩토리 메서드 패턴은 구체적인 클래스를 직접 생성하지 않고, 추상화된 팩토리 메서드를 통해 객체를 생성하기 때문에 고수준 모듈이 저수준 모듈에 의존하지 않도록 코딩할 수 있었습니다. - 의존 역전 원칙(DIP)
  • 객체 지향 프로그래밍에 대해 조금 더 다가가는 계기가 되었습니다.
profile
✅ 적당한 추상화를 찾아가는 개발자입니다.

0개의 댓글