Making a Calculator Engine in C# - Revisiting Software Design Principles

Making a Calculator Engine in C# - Revisiting Software Design Principles

This post introduces a C# implementation of a calculator engine and explores how it embodies various popular software design principles.

The project is available on GitHub at: https://github.com/yamesant/calculator/tree/checkpoints/checkpoint-0

The link references the checkpoint branch that corresponds to this blog post. While the main branch and the checkpoint-0 branch are equivalent at the time of publishing this blog post, they may diverge in the future.

Expression Interface Showcase

The main class is Expression. It represents an arithmetic expression.

Expressions can be constructed by passing a list of values and an operation:

var values = new List<double> { firstFactor, secondFactor };
var expression = Expression.CreateMultiValued(values, new Multiplication());

The base case is:

double value;
var expression = Expression.CreateSingleValued(value);

Expressions can also be nested. For example, \(\displaystyle\frac{5 * 4 - 2}{2 + 3 + 4}\) can be constructed as:

var expression = Expression.CreateNested(new List<Expression>()
	{
		Expression.CreateNested(new List<Expression>()
		{
			Expression.CreateMultiValued(new List<double> {5, 4}, new Multiplication()),
			Expression.CreateSingleValued(2),
		}, new Subtraction()),
		Expression.CreateMultiValued(new List<double> {2, 3, 4}, new Addition()),
	},
	new Division());

Once constructed, an expression can be evaluated. ExpressionTests.cs shows more examples how to use the class.

Terminology

In the context of software, an engine is the core component of a software application. Being the "core" implies two things. First, the engine implements certain functionality. Second, other software components rely on the engine to provide that functionality to them. Examples of these other software components include APIs, UIs, services, and scheduled jobs.

Software Design Principles

This section discusses encapsulation, open/closed principle, and composition over inheritance that the calculator engine project demonstrates.

Encapsulation

Encapsulation refers to protecting data from entering an invalid state. Information hiding and guard expressions are two examples (among many) of achieving encapsulation.

Information hiding means avoiding publicly exposing anything unnecessary. For example, the Expression class uses private fields _operation and _subexpression instead of exposing public properties. Similarly, the Operation class exposes Arity only to its subclasses.

Guard expressions are checks within a method that constructs or modifies a class. They ensure that the class remains valid after the changes. In this case, the Expression class can be constructed through a variety of static methods, all of which go through the private constructor. The constructor uses the following guard expression:

if (!operation.CanApply(subexpressions.Count))
	throw new ArgumentException("Operation arity does not match the number of arguments");

Open/Closed Principle

The open/closed principle (OCP), also referred to as polymorphic OCP, refers to a situation where a software component is open for extension but closed for modification. When this software component is a class (as opposed to, for example, a module or a function), the principle entails using an abstract base class and several derived classes that provide implementations.

The abstract base class serves as an interface between software components that use it (its dependants) and classes that inherit from it (its implementations). The abstract base class is closed for modification in the sense that changing it may require changing all of its dependants and implementations. However, it remains open for extension in the sense that new implementations can be added without changes to the existing codebase.

In this project, the OCP is realised in mathematical operations. The abstract base class is Operation:

public abstract class Operation
{
    protected abstract Arity Arity { get; }
    public abstract double Apply(List<double> values);

    public bool CanApply(int numberOfArguments)
    {
        if (Arity.IsAny) return true;
        return Arity.Value == numberOfArguments;
    }
}

Any implementation must specify the arity and provide an algorithm for Apply method.

Composition over Inheritance

Composition and inheritance are two ways, among many, of extending the functionality of a software application.

Composition involves constructing complex objects by combining simpler ones. One way to implement it is to have private fields reference instances of other classes. For example, the Expression class composes with itself and the Operation class.

Inheritance involves creating new classes based on existing ones, inheriting their properties and behaviours. For example, a potential alternate way to implement the Expression class is to subclass it for each different type of expression, such as SingleValued and MultiValued.

The principle of "Composition over Inheritance" states that when both composition and inheritance are viable, composition should be preferred.

Conclusion

This article has introduced an implementation of a calculator engine and has discussed how it relates to software design principles such as encapsulation, the open/closed principle, and composition over inheritance.

Following up are revision questions to reinforce recall of the key concepts.

What is an engine?It is the core component of a software application, upon which other components rely
What are two methods of achieving encapsulation?Information hiding and guard expressions
What does "open" mean in the OCP?"Open" means "easy". New implementations of the abstract base class can be added without modifying other parts of the codebase
What does "closed" mean in the OCP?"Closed" means "hard". Modifying an abstract base class may require modifying all of its dependants and all of its implementations

Read more