So, back to my RPN calculator implementation. Bear in mind that I wrote this code over six months ago, and I’ve learned a lot since then. If I was to do it again now I may make different design decisions, but for the moment I’ll stick with the original code. Let’s see what we can learn.
Last post I looked at the InputLine
class. A calculator also needs a set of operations it can perform. An operation can be an input operation, or can take 0, 1 or 2 operands from the operand stack (constant, unary or binary operation) and returns a value to the stack. Each operation is bound to a key on the calculator interface.
First things first, though.
1: using System;
2: using System.Collections.Generic;
3:
4: namespace LearningCSharp
5: {
6: public partial class RPNCalculatorEngine
7: {
8: public delegate double BinaryOp(double operand1, double operand2);
9: public delegate double UnaryOp(double operand);
10:
I declare a couple of delegate types, BinaryOp
and UnaryOp
. A delegate is similar to a function pointer in C. It basically gives you a way to refer to a method and pass it around to get stuff done. A variable of type BinaryOp
can refer to a method that takes two double parameters and return a double. A UnaryOp
takes one double parameter and returns a double. We’ll see these delegate types in use in a minute.
11: public class Operation
12: {
13: protected readonly string _name;
14: protected readonly string _abbreviation;
15: protected readonly string _keyText;
16: protected readonly char _keyboardShortCut;
17: protected readonly int _operandsRequired;
18:
19: public Operation(string name, string abbr, string keyText, char keyboardShortCut, int operandsRequired = 0)
20: {
21: _name = name;
22: _abbreviation = abbr;
23: _keyText = keyText;
24: _keyboardShortCut = keyboardShortCut;
25: _operandsRequired = operandsRequired;
26: }
27:
28: public string Name { get { return _name; } }
29: public string Abbreviation { get { return _abbreviation; } }
30: public string KeyText { get { return _keyText; } }
31: public char ShortCut { get { return _keyboardShortCut; } }
32: public virtual bool HasFunction { get { return false; } }
33:
34: public virtual void Execute(Stack<double> operands) { }
35: }
36:
In this design, an Operation is something that happens based on user interaction with the calculator interface. Above is the Operation
class, which is the base class for all Operations. The class includes read-only fields for the name, abbreviation, text to display on calculator interface (_keyText
), keyboard shortcut to bind to the operation (_keyboardShortCut
), and the number of operands required, if any, for the operation.
Declaring a field as readonly
means that it can only be set during object instantiation, that is, in the constructor. As there’s no way to modify the object once it’s been created, an Operation
object is immutable. Declaring the fields as protected
means that they can only be accessed directly by code within this class, or any class derived from this class.
The class also defines some public properties that give access to the values stored in the fields to code outside of the class. Note that _operandsRequired
doesn’t have an associated property, so it’s only available within the class, or those derived from it. Note also the HasFunction
property, which in this class returns false
. The purpose of this property is to indicate to client code whether or not this operation has a function associated with it. The property is declared virtual
so that it can be overridden by classes which derive from Operation
.
Finally, the class has a virtual method called Execute
, which in this case does nothing. In derived classes it will be overridden to execute the function associated with the operation.
37: public class BinaryOperation : Operation
38: {
39:
40: private readonly BinaryOp _function;
41:
42: public BinaryOperation(string name, string abbr, string keyText, char keyBoardShortCut, BinaryOp func)
43: : base(name, abbr, keyText, keyBoardShortCut, 2)
44: {
45: _function = func;
46: }
47:
48: public override bool HasFunction { get { return true; } }
49:
50: public override void Execute(Stack<double> operands)
51: {
52: if (operands.Count < _operandsRequired)
53: {
54: throw new ArgumentException(_abbreviation + ": Insufficient operands");
55: }
56: operands.Push(_function(operands.Pop(), operands.Pop()));
57: }
58: }
59:
Here’s the definition of the BinaryOperation
class. Line 37 declares the class to be derived from the Operation
class (: Operation
), so BinaryOperation
inherits all of Operation
’s public and protected members (fields, properties, methods, etc).
BinaryOperation
adds a new field to store a function to be performed by the operation. Note that the type of this member is one of the delegate types we declared earlier. So, an object of the BinaryOperation
class will store a reference to a procedure that accepts two double
parameters, and returns a double
result.
The constructor here takes a form that we haven’t seen before.
public BinaryOperation(string name, string abbr, string keyText, char keyBoardShortCut, BinaryOp func) : base(name, abbr, keyText, keyBoardShortCut, 2)
The constructor’s signature is similar to that of the Operation
constructor, except that instead of a parameter for the number of operands it accepts a delegate for the function to be associated with this operation. The next line defines the constructor initialiser. By default, all instance constructors (except for object
) call the parameterless constructor of the base class (base()
) before executing the body of the constructor. This allows the base class to handle the construction of its bit so the derived class doesn’t have to worry about it. In this case we change the default behaviour by specifying the constructor initialiser explicitly. This basically passes most of the parameters through to the Operation
constructor, including the specification of the number of operands needed for this operation. The body of the constructor then initialises the only thing specific to this class, the _function
field.
In order for the calculator to be able to make use of this function the BinaryOperator
class overrides the Execute
method from Operation
. The calculator engine passes the operand stack to the Execute method. The method checks that there are sufficient operands on the stack, and throws an exception if not. The exception includes an error message that can be used by the calculator engine.
If there are enough operands on the stack, Execute
pop’s the top two operands, calls the function stored in the operation, and then pushes the result onto the stack. Pretty simple.
60: public class UnaryOperation : Operation
61: {
62:
63: private readonly UnaryOp _function;
64:
65: public UnaryOperation(string name, string abbr, string keyText, char keyBoardShortCut, UnaryOp func)
66: : base(name, abbr, keyText, keyBoardShortCut, 1)
67: {
68: _function = func;
69: }
70:
71: public override bool HasFunction { get { return true; } }
72:
73: public override void Execute(Stack<double> operands)
74: {
75: if (operands.Count < _operandsRequired)
76: {
77: throw new ArgumentException(_abbreviation + ": Insufficient operands");
78: }
79: operands.Push(_function(operands.Pop()));
80: }
81: }
82:
The UnaryOperation
class loos very similar to BinaryOperation
. The basic differences are the delegate type passed to the constructor, the number of required operands passed to the base constructor, and operation of the Execute
method. In this class Execute
pops a single operand off the stack and returns the result of the function to the stack.
83: public class ConstantOperation : Operation
84: {
85:
86: private readonly double _value;
87:
88: public ConstantOperation(string name, string abbr, string keyText, char keyBoardShortCut, double value)
89: : base(name, abbr, keyText, keyBoardShortCut, 0)
90: {
91: _value = value;
92: }
93:
94: public override bool HasFunction { get { return true; } }
95:
96: public override void Execute(Stack<double> operands)
97: {
98: operands.Push(_value);
99: }
100: }
101: }
102: }
The ConstantOperation
class is similar to the two above except that instead of a function delegate, it stores a constant value. The Execute
method just pushes that value onto the operand stack.
So there we are, we’ve looked at the implementation of the Operations and the InputLine for the calculator. Next time we’ll look at the rest of the calculator engine and see how all this ties together.
Before I go, though, a couple of notes on the way this design would change if I were doing it today. Since I designed these classes I’ve been learning a lot about the .Net framework. In the System
namespace are a bunch of classes which use generics to define easy to use delegates. Instead of declaring the BinaryOp
and UnaryOp
delegates we can use Func<double, double, double>
and Func<double, double>
. And for consistency we could use Func<double>
instead of the value parameter for the ConstantOperation
class.
See you next time.
No comments:
Post a Comment