3
\$\begingroup\$

I am aiming to implement a calculator in an object-oriented way.

Here is the solution by using the strategy pattern.

Looking forward to some valuable comments.

package oopdesign.calculator;

public class AdditionStrategy implements CalculationStrategy {

    @Override
    public int calculate(int value1, int value2) {
        return value1 + value2;
    }
}


package oopdesign.calculator;

public interface CalculationStrategy {

    int calculate(int value1, int value2);
}


package oopdesign.calculator;

public class Calculator {

    public static Calculator instance = null;

    CalculationStrategy calculationStrategy;

    public void setCalculationStrategy(CalculationStrategy calculationStrategy) {
        this.calculationStrategy = calculationStrategy;
    }

    public static Calculator getInstance(){
        if(instance == null){
            instance = new Calculator();
        }
        return instance;
    }

    public int calculate(int value1, int value2) {
       return calculationStrategy.calculate(value1, value2);
    }
}


package oopdesign.calculator;

public class CalculatorMain {

    public static void main(String[] args) {

        Calculator c = Calculator.getInstance();

        c.setCalculationStrategy(new AdditionStrategy());
        System.out.println(c.calculate(5 ,2));

        c.setCalculationStrategy(new SubtractionStrategy());
        System.out.println(c.calculate(5 ,2));

        c.setCalculationStrategy(new MultiplicationStrategy());
        System.out.println(c.calculate(5 ,2));

        c.setCalculationStrategy(new DivideStrategy());
        System.out.println(c.calculate(5 ,2));
    }
}


package oopdesign.calculator;

public class DivideStrategy implements CalculationStrategy {

    @Override
    public int calculate(int value1, int value2) {
        return value1 / value2;
    }
}


package oopdesign.calculator;

public class MultiplicationStrategy implements  CalculationStrategy{

    @Override
    public int calculate(int value1, int value2) {
        return value1 * value2;
    }
}

package oopdesign.calculator;

public class SubtractionStrategy implements CalculationStrategy {

    @Override
    public int calculate(int value1, int value2) {
        return value1 - value2;
    }
}
\$\endgroup\$

4 Answers 4

2
\$\begingroup\$

Review:

From extensibility and in pro of having the possibility of including in future versions more operations, it is a good approach. The code is very simple, and it is easy to read so it is very good your design proposal.

The main purpose of applying design patterns is to simplify things, to reach the maximum level of abstraction and allow you to write meaningful code, not just repeat stuff

So, you did good.

However, there are some observations:

package oopdesign.calculator;

//Singleton is a good approach for this problem
public class Calculator {

    //By default any object is null
    //Do not put it as public, you have the getInstance method
    private static Calculator instance;

    //You are limiting the operations to handle
    CalculationStrategy calculationStrategy;

    //This is not a Singleton if you allow the default constructor (its public by default)
    private Calculator() {
    }

    public void setCalculationStrategy(CalculationStrategy calculationStrategy) {
        this.calculationStrategy = calculationStrategy;
    }

    public static Calculator getInstance() {
        if (instance == null)
            instance = new Calculator();
        return instance;
    }

    //You should think about handle the most general data type (this case double)
    public double calculate(double value1, double value2) {
       return calculationStrategy.calculate(value1, value2);
    }
}
package oopdesign.calculator;

public class CalculatorMain {

    public static void main(String[] args) {

        Calculator c = Calculator.getInstance();

        //There is a problem with it, you need to instanciate the strategies
        //each time you need to use it
        c.setCalculationStrategy(new AdditionStrategy());
        System.out.println(c.calculate(5,2));

        //It requires space, plus you are not being efficient by storing
        //there operations (calculation strategies)
        c.setCalculationStrategy(new SubtractionStrategy());
        System.out.println(c.calculate(5,2));

        c.setCalculationStrategy(new MultiplicationStrategy());
        System.out.println(c.calculate(5,2));

        c.setCalculationStrategy(new DivideStrategy());
        System.out.println(c.calculate(5,2));
    }
}

An alternative

import java.util.HashMap;
import java.util.Map;

public class Calculator {

    private static Calculator instance;

    //search in Constant time (approximately)
    private Map<String, CalculationStrategy> calculationStrategies;

    private Calculator() {
        calculationStrategies = new HashMap<>();
    }

    public void addCalculationStrategy(String name, CalculationStrategy strategy) {
        calculationStrategies.put(name, strategy);
    }

    public static Calculator getInstance() {
        if (instance == null)
            instance = new Calculator();
        return instance;
    }

    //double b... means that there may be 0 to n parameters
    //consider that there are unitary operators or functions in a calculator
    public double calculate(String name, double a, double... b) {
        return calculationStrategies.get(name).calculate(a, b);
    }
}
package oopdesign.calculator;

public class Main {

    public static void main(String[] args) {
        Calculator calculator = Calculator.getInstance();

        //Use a lambda instead
        calculator.addCalculationStrategy("+", (a, b) -> a + b[0]);
        //[b] is taken as an array but is a variadic parameter
        calculator.addCalculationStrategy("-", (a, b) -> a - b[0]);
        calculator.addCalculationStrategy("*", (a, b) -> a * b[0]);
        calculator.addCalculationStrategy("/", (a, b) -> a / b[0]);
        calculator.addCalculationStrategy("Abs", (a, b) -> Math.abs(a));
        calculator.addCalculationStrategy("Cos", (a, b) -> Math.cos(a));
        calculator.addCalculationStrategy("Sin", (a, b) -> Math.sin(a));

        System.out.println(calculator.calculate("+", 1, 3));
        System.out.println(calculator.calculate("-", 1, 3));
        System.out.println(calculator.calculate("*", 1, 3));
        System.out.println(calculator.calculate("/", 1, 3));
        System.out.println(calculator.calculate("Abs", -66));
        System.out.println(calculator.calculate("Cos", 75));
        System.out.println(calculator.calculate("Sin", 28));
        System.out.println(calculator.calculate("+", 666, 777));
    }
}

About double b... read this post about Variadic function parameters, as I said, it is a way to have multiple parameters, From 0 To N parameters

Thanks for reading this answer.

\$\endgroup\$
3
\$\begingroup\$

Readability and ease of use

I think the calculator should have simple functions : plus, minus, divide, multiple and use the strategy within the calculator.

State

You are setting the operation by a state in the calculator. Change state can lead to weird bugs.

For example what will happen if you call calculator.Calculate without calling setCalculationStategy?

Singleton

I don't understand why the calculator is a singleton? What are the benefits?

Think what will happen if you use it with multiple threads.

\$\endgroup\$
2
\$\begingroup\$

I like they way you thought about the problem but it has some downsides..

(The answer will only focus on the Strategy Design Pattern and ignores the use of the Singleton Pattern)


Without the Strategy Pattern

Let us compare the design you provide with a different approach:

class Calculator {

    int add(int a, int b) {
        return a + b;
    }

    int substract(int a, int b) {
        return a - b;
    }

    int multiply(int a, int b) {
        return a * b;
    }

    int divide(int a, int b) {
        return a / b;
    }
}

class Main {

    public static void main(String... args) {
        Calculator c = new Calculator();

        System.out.println(c.add(5 ,2));

        System.out.println(c.substract(5 ,2));

        System.out.println(c.multiply(5 ,2));

        System.out.println(c.divide(5 ,2));
    }

}

The benefits of the new approach are:

  • only 2 instead of 7 classes
  • simply usage - no need to change the strategy for each operation

As you can see the strategy pattern adds to much complexity to this simple problem.

Strategy Pattern is not made for this Use Case

The benefit of the Strategy Design Pattern is that it enables selecting an algorithm at runtime.

But algorithm is not meant to switch between different types of calculations; it is much more about switching between different behaviors for different types of calculators.

A Possible Use Case

Imagine you want to sell your calculator and a potential customer has a 7-day trial period before he has to buy it. During the trial period the customer can only use add and subtract. If the customer does not buy the calculator after the trial period, no methods can be used.

For this problem presentation we could have three types of calculators:

  • trail-calculator
  • purchased calculator
  • unpurchased calculator

First Try without the Strategy Pattern

We could create 3 classes (to make it easy, I'll just demonstrate with add) and then we'll see the downside:

class PurchasedCalculator {

    int add(int a, int b) {
        return a + b;
    }

}

class UnpurchasedCalculator {

    int add(int a, int b) {
        throw NotPurchasedExecption()
    }

}

class TrialCalculator {
    
    int add(int a, int b) {
        return a + b;
    }

    int multiply(int a, int b) {
       throw NotPurchasedExecption();
    }
    
}

The downside of this approach is that we have many code duplication every where.

Second Try with the Strategy Pattern

To avoid code duplication and the flexibility not to create a new class for each calculator type, we can use the Strategy Pattern:

class Calculator {
    /* ... */
    
    Calculator(CalculationStrategy additionStrategy,
               CalculationStrategy substractionStrategy,
               CalculationStrategy multiplicationStrategy,
               CalculationStrategy dividitionStrategy) {
        this.additionStrategy = additionStrategy;
        this.substractionStrategy = substractionStrategy;
        this.multiplicationStrategy = multiplicationStrategy;
        this.divideStrategy = divideStrategy;
    }

    int add(int a, int b) {
        return additionStrategy.calculate(a, b);
    }

    /* ... */
}

We can easy create different calculator types:

class Main {

    public static void main(String... args) {

        Calculator trial = new Calculator(new AdditionStrategy(), 
                                          new SubstractionStrategy(),
                                          new NotPurchasedStrategy(),
                                          new NotPurchasedStrategy());

        Calculator purchased = new Calculator(new AdditionStrategy(), 
                                              new SubstractionStrategy(),
                                              new MultiplicationStrategy(),
                                              new DividitionStrategy());

        Calculator unpurchased = new Calculator(new NotPurchasedStrategy(), 
                                                new NotPurchasedStrategy(),
                                                new NotPurchasedStrategy(),
                                                new NotPurchasedStrategy());

    }

}

Or modify the behavior at runtime - for instance the customer did not pay his subscription:

Calculator purchased = new Calculator(new AdditionStrategy(), 
                                      new SubstractionStrategy(),
                                      new MultiplicationStrategy(),
                                      new DividitionStrategy());

purchased.setAdditionStrategy(new NotPurchasedStrategy());
/*...*/
\$\endgroup\$
1
\$\begingroup\$

What about designing it in such a way that more operations can be added later and different calculation logic can be implemented by extending the base class.

public class TestCalculator {
    public static void main(String[] args) {
        //list of operator
        List<Operator> operatorList = new LinkedList<>();
        operatorList.add(new Add());
        operatorList.add(new Multiply());

        //operator precedence, Assuming highest precedence operator is at lower index
        List<Character> operatorPrecedence = new ArrayList<>();
        operatorPrecedence.add('*');
        operatorPrecedence.add('+');

        // initialise SimpleExpressionCalculator
        ExpressionCalculator expressionCalculator = new SimpleExpressionCalculator(operatorList,operatorPrecedence);
        System.out.println(expressionCalculator.calculate("2 + 5 * 3 * 2 + 2"));
    }
}

interface Calculator {
    Double calculate(String expression);
}

class SimpleExpressionCalculator implements Calculator {
    Map<Character,Operator> operatorMap;
    List<Character> operatorPrecedence;

    public SimpleExpressionCalculator(List<Operator> operators,List<Character> operatorPrecedence ) {
        operatorMap = new HashMap<>();
        for(Operator o : operators) {
            operatorMap.put(o.getSymbol(),o);
        }
        this.operatorPrecedence = operatorPrecedence;
    }

    @Override
    public Double calculate(String expression) {
        // logic to parse the expression and return result
        return 0.0;
    }
}

interface Operator {
    public Double operate(Double d1, Double d2);
    public Character getSymbol();
}

class Add implements Operator {
    @Override
    public Double operate(Double d1, Double d2) {
        return d1 + d2;
    }

    @Override
    public Character getSymbol() {
        return '+';
    }
}


class Multiply implements Operator {
    @Override
    public Double operate(Double d1, Double d2) {
        return d1 * d2;
    }

    @Override
    public Character getSymbol() {
        return '*';
    }
}
```
\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.