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.