In this article, you will learn about a way of thinking about programming that reduces the number of conditional statements in your code and improves testability.
Introduction
State Oriented Programming is a way of thinking about programming not as process flow but as state and behaviour. With traditional programming, and by traditional, I include procedural, functional and OO programming styles, the code will contain conditional statements, usually in the form of ‘if
’ statements, with possibly, case
/switch
statements, but it will almost certainly contain some form of logic to decide whether an action should be performed or not. The purpose of the conditional code will be varied, but it would be a strange program that did not contain any conditional statements.
However, if we analyze these Conditional Statements, what they are actually saying is ‘If object/code is in this state, then do ‘X’ if not then do ‘Y’.
State Oriented Programming tries (and I will come on to the ‘tries’ in a moment) to change that narrative by replacing the conditional statements with a call to a function, where the function that will be called is determined ahead of time, whenever the state of the program/object changes.
So, instead of the ‘If
-Then
-Else
’ clause (or something similar), we have ‘Execute_Function()
’, where the function being executed has changed to match the object state.
As an example, consider modelling UK traffic lights:
A traditional C# code block might look like the following:
namespace TrafficLight
{
public enum Switch { Off, On }
public enum Signal { Stop = 0, ReadyGo, Go, ReadyStop };
public class ClassicTrafficLight
{
private Signal currentSignal;
public ClassicTrafficLight()
{
SwitchRed(Switch.On);
currentSignal = Signal.Stop;
}
public void ChangeSignal(Signal currentSignal)
{
switch (currentSignal)
{
case Signal.Stop:
SwitchAmber(Switch.On);
currentSignal = Signal.ReadyGo;
break;
case Signal.ReadyGo:
SwitchRed(Switch.Off);
SwitchAmber(Switch.Off);
SwitchGreen(Switch.On);
currentSignal = Signal.Go;
break;
case Signal.Go:
SwitchGreen(Switch.Off);
SwitchAmber(Switch.On);
currentSignal = Signal.ReadyStop;
break;
case Signal.ReadyStop:
SwitchAmber(Switch.Off);
SwitchRed(Switch.On);
currentSignal = Signal.Stop;
break;
default:
throw new Exception("Unknown signal state");
}
}
}
}
Using an SOP approach, we have:
using System;
namespace SOPTrafficLight
{
public enum Switch { Off, On }
public enum Signal { Stop = 0, ReadyGo, Go, ReadyStop };
public class SOPTrafficLight
{
private Func <Signal>[] signalChanges;
private Signal currentSignal;
public SOPTrafficLight()
{
signalChanges =
new Func<Signal>[] { Stop, ReadyGo, Go, ReadyStop };
currentSignal = Signal.Stop;
SwitchRed(Switch.On);
}
public void ChangeSignal()
{
currentSignal = signalChanges[(int)currentSignal]();
}
private Signal Stop()
{
SwitchAmber(Switch.On);
return Signal.ReadyGo;
}
private Signal ReadyGo()
{
SwitchRed(Switch.Off);
SwitchAmber(Switch.Off);
SwitchGreen(Switch.On);
return Signal.Go;
}
private Signal Go()
{
SwitchGreen(Switch.Off);
SwitchAmber(Switch.On);
return Signal.ReadyStop;
}
private Signal ReadyStop()
{
SwitchAmber(Switch.Off);
SwitchRed(Switch.On);
return Signal.Stop;
}
}
}
First thing to notice: Examine the ChangeSignal
method. With S.O.P., it is a one line method, and the whole code contains no conditional statements. No ‘IF
‘s, no ‘Switch
/Case
‘, no conditional statements at all!!! How cool is that.
OK – so I hear you say that all I have done is encapsulate the code in the case clauses within functions. On the actual lines of functioning code, that is (possibly) a valid viewpoint. But did you even think you could model a set of traffic lights without conditional statements. Be honest, you didn’t, did you?
What we have done is to tie functions to state. Every state change of the traffic light is encapsulated in a state change function. It is triggered in isolation from every other possible state, and, the most important point, the function that will be executed next is determined ahead of it being executed.
There is no code that is saying “if in this state, then do this”. The state that the traffic light is in dictates the action it will take, and that action is pre-determined.
Now the traffic light scenario has a limited number of states and state transitions. Increase the numbers of states and transitions, I would argue that an S.O.P. approach very quickly produces code that is easier to maintain, understand, and, crucially, Test.
More Complex State Maps, More reason for S.O.P.
The traffic light above is an example of a simple sequential state changing object. The traffic light only ever executes the one line in the ChangeState
function. All we instruct the object to do is move from its current state to the next state.
However, objects’ states are rarely this simple, More often than not, an object will have a matrix of states. So let's examine a slightly more complex example: A Calculator.
Calculator
I have chosen a calculator because:
- It is something familiar to us all.
- It is self contained.
- We can start with a limited functionality and then expand on that.
For the initial implementation, I am restricting the functionality to accepting the following inputs:
- Numerics: (Numbers 0-9)
- Fraction indicator: (.)
- Binary operators: (+, -, /, *)
- Result Operator (=)
- Clear accumulator (I know – the above image is missing a ‘Clear’ button, let's assume it is on the side.)
Even with this limited set of inputs, we already have a number of state issues to contend with:
- The Fraction indicator (decimal point) is only valid in certain circumstances.
- The Fraction indicator (decimal point) determines the effective value of the next numeric input.
- A Binary Operator cannot follow a Binary Operator.
- The Result operator cannot follow a Binary Operator.
- The earliest the execution of Binary Operator can take place is when input of the second operand has been completed.
- A Numeric input following a completed calculation means start a new calculation, whilst a Binary Operator input means take the current result as being the first operand.
Calculator Functionality
The Calculator will have as its initial condition a value = 0.
If the first key pressed at the start of a calculation is an Operator, then the Calculator value will be used as the value of the first Operand. If the first key pressed is Numeric, then it will be assumed that a new calculation is being started and any existing value will be discarded.
Calculator States
If we consider the defining of the operands as being the main focus of the calculation states, and the non numeric keys – Operators, Decimal Point, ‘=’ and Clear as state transition triggers, possibly with associated transition actions, then we have nine possible states:
- Initial Start up
- Defining integral numeric of first operand
- Defining decimal part of first operand
- Defining first integral numeric of second operand
- Defining subsequent integral numeric of second operand.
- Defining decimal part of second operand.
- Calculation Completed
- Reset (Cleared)
- Errored
Now whilst we have nine possible states, if we consider the initial state equal to a calculated state with a result of zero, and the Reset and Errored states taking us back to the Initial state, then the states numbered 1, 7, 8, and 9 are in effect identical, and that brings us down to six states. (Renamed State0
- State5
to match code)
State0
: Start of Calculation State1
: Defining Integral part of first operand State2
: Defining Decimal part of first operand State3
: Defining First Integral of second operand State4
: Defining Subsequent Integral part of second operand State5
: Defining Decimal part of second operand
The six states have the following possible state transitions:
State0
- 0-9 ->
State1
- Dec pt ->
State1
- Operator ->
State3
- Clear or = ->
State0
State1
- 0-9 ->
State1
- Dec pt ->
State2
- Operator ->
State3
- Clear or = ->
State0
State2
- 0-9 ->
State2
- Dec Pt -> Error, then
State0
- Operator ->
State3
- Clear or = ->
State0
State3
- 0-9 ->
State4
- Dec Pt ->
State5
- Operator -> Error, then
State0
- Clear or = ->
State0
State4
- 0-9 ->
State4
- Dec pt ->
State5
- Operator ->
State3
- Clear or = ->
State0
State5
- 0-9 ->
State5
- Dec Pt -> Error, then
State0
- Operator ->
State3
- Clear or = ->
State0
Calculator State Actions
I am proposing a very simplistic implementation in which for each of the six states, we create a dictionary of Action and TransitionState for each of the seventeen possible keys:
- Numeric (0 – 9)
- Decimal point
- Operators (+ – * /)
- Result
- Clear
state0 = new Dictionary<char, StateAction>
{
{ '0', new StateAction(StartOfNewCalculation, States.State1) },
{ '1', new StateAction(StartOfNewCalculation, States.State1) },
{ '2', new StateAction(StartOfNewCalculation, States.State1) },
{ '3', new StateAction(StartOfNewCalculation, States.State1) },
{ '4', new StateAction(StartOfNewCalculation, States.State1) },
{ '5', new StateAction(StartOfNewCalculation, States.State1) },
{ '6', new StateAction(StartOfNewCalculation, States.State1) },
{ '7', new StateAction(StartOfNewCalculation, States.State1) },
{ '8', new StateAction(StartOfNewCalculation, States.State1) },
{ '9', new StateAction(StartOfNewCalculation, States.State1) },
{ '.', new StateAction(StartOfNewCalculation, States.State1) },
{ '+', new StateAction(Operator, States.State3) },
{ '-', new StateAction(Operator, States.State3) },
{ '*', new StateAction(Operator, States.State3) },
{ '/', new StateAction(Operator, States.State3) },
{ '=', new StateAction(Result, States.State0) },
{ 'C', new StateAction(Clear, States.State0) }
};
state1 = new Dictionary<char, StateAction>
{
{ '0', new StateAction(FirstIntegralDigit, States.State1) },
{ '1', new StateAction(FirstIntegralDigit, States.State1) },
{ '2', new StateAction(FirstIntegralDigit, States.State1) },
{ '3', new StateAction(FirstIntegralDigit, States.State1) },
{ '4', new StateAction(FirstIntegralDigit, States.State1) },
{ '5', new StateAction(FirstIntegralDigit, States.State1) },
{ '6', new StateAction(FirstIntegralDigit, States.State1) },
{ '7', new StateAction(FirstIntegralDigit, States.State1) },
{ '8', new StateAction(FirstIntegralDigit, States.State1) },
{ '9', new StateAction(FirstIntegralDigit, States.State1) },
{ '.', new StateAction(DecimalPoint, States.State2) },
{ '+', new StateAction(Operator, States.State3) },
{ '-', new StateAction(Operator, States.State3) },
{ '*', new StateAction(Operator, States.State3) },
{ '/', new StateAction(Operator, States.State3) },
{ '=', new StateAction(Result, States.State0) },
{ 'C', new StateAction(Clear, States.State0) }
};
state2 = new Dictionary<char, StateAction>
{
{ '0', new StateAction(FirstFractionalDigit, States.State2) },
{ '1', new StateAction(FirstFractionalDigit, States.State2) },
{ '2', new StateAction(FirstFractionalDigit, States.State2) },
{ '3', new StateAction(FirstFractionalDigit, States.State2) },
{ '4', new StateAction(FirstFractionalDigit, States.State2) },
{ '5', new StateAction(FirstFractionalDigit, States.State2) },
{ '6', new StateAction(FirstFractionalDigit, States.State2) },
{ '7', new StateAction(FirstFractionalDigit, States.State2) },
{ '8', new StateAction(FirstFractionalDigit, States.State2) },
{ '9', new StateAction(FirstFractionalDigit, States.State2) },
{ '.', new StateAction(DecimalPointNotAllowed, States.State0) },
{ '+', new StateAction(Operator, States.State3) },
{ '-', new StateAction(Operator, States.State3) },
{ '*', new StateAction(Operator, States.State3) },
{ '/', new StateAction(Operator, States.State3) },
{ '=', new StateAction(Result, States.State0) },
{ 'C', new StateAction(Clear, States.State0) }
};
state3 = new Dictionary<char, StateAction>
{
{ '0', new StateAction(IntegralDigit, States.State4) },
{ '1', new StateAction(IntegralDigit, States.State4) },
{ '2', new StateAction(IntegralDigit, States.State4) },
{ '3', new StateAction(IntegralDigit, States.State4) },
{ '4', new StateAction(IntegralDigit, States.State4) },
{ '5', new StateAction(IntegralDigit, States.State4) },
{ '6', new StateAction(IntegralDigit, States.State4) },
{ '7', new StateAction(IntegralDigit, States.State4) },
{ '8', new StateAction(IntegralDigit, States.State4) },
{ '9', new StateAction(IntegralDigit, States.State4) },
{ '.', new StateAction(DecimalPoint, States.State5) },
{ '+', new StateAction(OperatorNotAllowed, States.State0) },
{ '-', new StateAction(OperatorNotAllowed, States.State0) },
{ '*', new StateAction(OperatorNotAllowed, States.State0) },
{ '/', new StateAction(OperatorNotAllowed, States.State0) },
{ '=', new StateAction(Result, States.State0) },
{ 'C', new StateAction(Clear, States.State0) }
};
state4 = new Dictionary<char, StateAction>
{
{ '0', new StateAction(IntegralDigit, States.State4) },
{ '1', new StateAction(IntegralDigit, States.State4) },
{ '2', new StateAction(IntegralDigit, States.State4) },
{ '3', new StateAction(IntegralDigit, States.State4) },
{ '4', new StateAction(IntegralDigit, States.State4) },
{ '5', new StateAction(IntegralDigit, States.State4) },
{ '6', new StateAction(IntegralDigit, States.State4) },
{ '7', new StateAction(IntegralDigit, States.State4) },
{ '8', new StateAction(IntegralDigit, States.State4) },
{ '9', new StateAction(IntegralDigit, States.State4) },
{ '.', new StateAction(DecimalPoint, States.State5) },
{ '+', new StateAction(CalcAndOperator, States.State3) },
{ '-', new StateAction(CalcAndOperator, States.State3) },
{ '*', new StateAction(CalcAndOperator, States.State3) },
{ '/', new StateAction(CalcAndOperator, States.State3) },
{ '=', new StateAction(CalcAndResult, States.State0) },
{ 'C', new StateAction(Clear, States.State0) }
};
state5 = new Dictionary<char, StateAction>
{
{ '0', new StateAction(FractionalDigit, States.State5) },
{ '1', new StateAction(FractionalDigit, States.State5) },
{ '2', new StateAction(FractionalDigit, States.State5) },
{ '3', new StateAction(FractionalDigit, States.State5) },
{ '4', new StateAction(FractionalDigit, States.State5) },
{ '5', new StateAction(FractionalDigit, States.State5) },
{ '6', new StateAction(FractionalDigit, States.State5) },
{ '7', new StateAction(FractionalDigit, States.State5) },
{ '8', new StateAction(FractionalDigit, States.State5) },
{ '9', new StateAction(FractionalDigit, States.State5) },
{ '.', new StateAction(DecimalPointNotAllowed, States.State0) },
{ '+', new StateAction(CalcAndOperator, States.State3) },
{ '-', new StateAction(CalcAndOperator, States.State3) },
{ '*', new StateAction(CalcAndOperator, States.State3) },
{ '/', new StateAction(CalcAndOperator, States.State3) },
{ '=', new StateAction(CalcAndResult, States.State0) },
{ 'C', new StateAction(Clear, States.State0) }
};
To enable us to have common operator code, and yet execute specific functions for specific operators, we create a dictionary of Operator Functions.
OperatorFunctions = new Dictionary<char, Func<double, double, double>> {
{ '+', PlusOperator},
{ '-', MinusOperator },
{ '*', MultiplicationOperator },
{ '/', DivisionOperator } };
Plus the transition functions – but they are small self-contained code snippets.
Putting the whole thing together, including the functions to execute the operators and the StateAction
class, we have the following as a fully functioning CalculatorEngine
– with NO conditionals.
using System;
using System.Collections.Generic;
namespace Calculator.Models
{
public enum States { State0 = 0, State1, State2, State3, State4, State5 };
public class CalculatorEngine
{
private char operatorInWaiting;
private double currentOperand;
private double fractionalDivisor;
private double accumulator;
private States state;
private IDictionary<char, StateAction> state0;
private IDictionary<char, StateAction> state1;
private IDictionary<char, StateAction> state2;
private IDictionary<char, StateAction> state3;
private IDictionary<char, StateAction> state4;
private IDictionary<char, StateAction> state5;
private IDictionary<States, IDictionary<char, StateAction>> stateSet;
private IDictionary<char, Func<double, double, double>> OperatorFunctions;
public CalculatorEngine()
{
state0 = new Dictionary<char, StateAction>
{
{ '0', new StateAction(StartOfNewCalculation, States.State1) },
{ '1', new StateAction(StartOfNewCalculation, States.State1) },
{ '2', new StateAction(StartOfNewCalculation, States.State1) },
{ '3', new StateAction(StartOfNewCalculation, States.State1) },
{ '4', new StateAction(StartOfNewCalculation, States.State1) },
{ '5', new StateAction(StartOfNewCalculation, States.State1) },
{ '6', new StateAction(StartOfNewCalculation, States.State1) },
{ '7', new StateAction(StartOfNewCalculation, States.State1) },
{ '8', new StateAction(StartOfNewCalculation, States.State1) },
{ '9', new StateAction(StartOfNewCalculation, States.State1) },
{ '.', new StateAction(StartOfNewCalculation, States.State1) },
{ '+', new StateAction(Operator, States.State3) },
{ '-', new StateAction(Operator, States.State3) },
{ '*', new StateAction(Operator, States.State3) },
{ '/', new StateAction(Operator, States.State3) },
{ '=', new StateAction(Result, States.State0) },
{ 'C', new StateAction(Clear, States.State0) }
};
state1 = new Dictionary<char, StateAction>
{
{ '0', new StateAction(FirstIntegralDigit, States.State1) },
{ '1', new StateAction(FirstIntegralDigit, States.State1) },
{ '2', new StateAction(FirstIntegralDigit, States.State1) },
{ '3', new StateAction(FirstIntegralDigit, States.State1) },
{ '4', new StateAction(FirstIntegralDigit, States.State1) },
{ '5', new StateAction(FirstIntegralDigit, States.State1) },
{ '6', new StateAction(FirstIntegralDigit, States.State1) },
{ '7', new StateAction(FirstIntegralDigit, States.State1) },
{ '8', new StateAction(FirstIntegralDigit, States.State1) },
{ '9', new StateAction(FirstIntegralDigit, States.State1) },
{ '.', new StateAction(DecimalPoint, States.State2) },
{ '+', new StateAction(Operator, States.State3) },
{ '-', new StateAction(Operator, States.State3) },
{ '*', new StateAction(Operator, States.State3) },
{ '/', new StateAction(Operator, States.State3) },
{ '=', new StateAction(Result, States.State0) },
{ 'C', new StateAction(Clear, States.State0) }
};
state2 = new Dictionary<char, StateAction>
{
{ '0', new StateAction(FirstFractionalDigit, States.State2) },
{ '1', new StateAction(FirstFractionalDigit, States.State2) },
{ '2', new StateAction(FirstFractionalDigit, States.State2) },
{ '3', new StateAction(FirstFractionalDigit, States.State2) },
{ '4', new StateAction(FirstFractionalDigit, States.State2) },
{ '5', new StateAction(FirstFractionalDigit, States.State2) },
{ '6', new StateAction(FirstFractionalDigit, States.State2) },
{ '7', new StateAction(FirstFractionalDigit, States.State2) },
{ '8', new StateAction(FirstFractionalDigit, States.State2) },
{ '9', new StateAction(FirstFractionalDigit, States.State2) },
{ '.', new StateAction(DecimalPointNotAllowed, States.State0) },
{ '+', new StateAction(Operator, States.State3) },
{ '-', new StateAction(Operator, States.State3) },
{ '*', new StateAction(Operator, States.State3) },
{ '/', new StateAction(Operator, States.State3) },
{ '=', new StateAction(Result, States.State0) },
{ 'C', new StateAction(Clear, States.State0) }
};
state3 = new Dictionary<char, StateAction>
{
{ '0', new StateAction(IntegralDigit, States.State4) },
{ '1', new StateAction(IntegralDigit, States.State4) },
{ '2', new StateAction(IntegralDigit, States.State4) },
{ '3', new StateAction(IntegralDigit, States.State4) },
{ '4', new StateAction(IntegralDigit, States.State4) },
{ '5', new StateAction(IntegralDigit, States.State4) },
{ '6', new StateAction(IntegralDigit, States.State4) },
{ '7', new StateAction(IntegralDigit, States.State4) },
{ '8', new StateAction(IntegralDigit, States.State4) },
{ '9', new StateAction(IntegralDigit, States.State4) },
{ '.', new StateAction(DecimalPoint, States.State5) },
{ '+', new StateAction(OperatorNotAllowed, States.State0) },
{ '-', new StateAction(OperatorNotAllowed, States.State0) },
{ '*', new StateAction(OperatorNotAllowed, States.State0) },
{ '/', new StateAction(OperatorNotAllowed, States.State0) },
{ '=', new StateAction(Result, States.State0) },
{ 'C', new StateAction(Clear, States.State0) }
};
state4 = new Dictionary<char, StateAction>
{
{ '0', new StateAction(IntegralDigit, States.State4) },
{ '1', new StateAction(IntegralDigit, States.State4) },
{ '2', new StateAction(IntegralDigit, States.State4) },
{ '3', new StateAction(IntegralDigit, States.State4) },
{ '4', new StateAction(IntegralDigit, States.State4) },
{ '5', new StateAction(IntegralDigit, States.State4) },
{ '6', new StateAction(IntegralDigit, States.State4) },
{ '7', new StateAction(IntegralDigit, States.State4) },
{ '8', new StateAction(IntegralDigit, States.State4) },
{ '9', new StateAction(IntegralDigit, States.State4) },
{ '.', new StateAction(DecimalPoint, States.State5) },
{ '+', new StateAction(CalcAndOperator, States.State3) },
{ '-', new StateAction(CalcAndOperator, States.State3) },
{ '*', new StateAction(CalcAndOperator, States.State3) },
{ '/', new StateAction(CalcAndOperator, States.State3) },
{ '=', new StateAction(CalcAndResult, States.State0) },
{ 'C', new StateAction(Clear, States.State0) }
};
state5 = new Dictionary<char, StateAction>
{
{ '0', new StateAction(FractionalDigit, States.State5) },
{ '1', new StateAction(FractionalDigit, States.State5) },
{ '2', new StateAction(FractionalDigit, States.State5) },
{ '3', new StateAction(FractionalDigit, States.State5) },
{ '4', new StateAction(FractionalDigit, States.State5) },
{ '5', new StateAction(FractionalDigit, States.State5) },
{ '6', new StateAction(FractionalDigit, States.State5) },
{ '7', new StateAction(FractionalDigit, States.State5) },
{ '8', new StateAction(FractionalDigit, States.State5) },
{ '9', new StateAction(FractionalDigit, States.State5) },
{ '.', new StateAction(DecimalPointNotAllowed, States.State0) },
{ '+', new StateAction(CalcAndOperator, States.State3) },
{ '-', new StateAction(CalcAndOperator, States.State3) },
{ '*', new StateAction(CalcAndOperator, States.State3) },
{ '/', new StateAction(CalcAndOperator, States.State3) },
{ '=', new StateAction(CalcAndResult, States.State0) },
{ 'C', new StateAction(Clear, States.State0) }
};
OperatorFunctions = new Dictionary<char, Func<double, double, double>> {
{ '+', PlusOperator},
{ '-', MinusOperator },
{ '*', MultiplicationOperator },
{ '/', DivisionOperator } };
stateSet = new Dictionary<States, IDictionary<char, StateAction>>()
{
{States.State0, state0 },
{States.State1, state1 },
{States.State2, state2 },
{States.State3, state3 },
{States.State4, state4 },
{States.State5, state5 }
};
ResetCalculator();
}
public double Calculate(char key)
{
stateSet[state][key].Action(key);
state = stateSet[state][key].TransitionToState;
return accumulator;
}
#region Key Functions
private void StartOfNewCalculation(char key)
{
accumulator = 0.0;
state = stateSet[state][key].TransitionToState;
stateSet[state][key].Action(key);
}
private void FirstIntegralDigit(char key)
{
accumulator = accumulator * 10.0 + Char.GetNumericValue(key);
}
private void DecimalPoint(char key)
{
}
private void FirstFractionalDigit(char key)
{
fractionalDivisor *= 10.0;
accumulator = accumulator + (Char.GetNumericValue(key) / fractionalDivisor);
}
private void Operator(char key)
{
ResetNumerics();
operatorInWaiting = key;
}
private void IntegralDigit(char key)
{
currentOperand = currentOperand * 10.0 + Char.GetNumericValue(key);
}
private void FractionalDigit(char key)
{
fractionalDivisor *= 10.0;
currentOperand = currentOperand + (Char.GetNumericValue(key) / fractionalDivisor);
}
private void CalcAndOperator(char key)
{
ExecuteStackedOperator();
Operator(key);
}
private void DecimalPointNotAllowed(char key)
{
ResetCalculator();
throw new Exception("Error: Decimal Point not valid");
}
private void OperatorNotAllowed(char key)
{
ResetCalculator();
throw new Exception("Error: Operator not valid");
}
private void Result(<span class="token keyword">char</span> key)
{
ResetNumerics();
}
private void CalcAndResult(<span class="token keyword">char</span> key)
{
ExecuteStackedOperator();
ResetNumerics();
}
private void Clear(<span class="token keyword">char</span> key)
{
ResetCalculator();
}
#endregion Key Functions
#region Operator Functions
private double PlusOperator(<double operand1, double operand2)
{
return operand1 + operand2;
}
private double MinusOperator(double operand1, double operand2)
{
return operand1 - operand2; ;
}
private double MultiplicationOperator(double operand1, double operand2)
{
return operand1 * operand2;
}
private double DivisionOperator(double operand1, double operand2)
{
return operand1 / operand2;
}
#endregion Operator Functions
#region State transition and Operator execution
private void TransitionState(States requiredState)
{
state = requiredState;
}
private void ExecuteStackedOperator()
{
accumulator = OperatorFunctions[operatorInWaiting](accumulator, currentOperand);
}
#endregion State transition and Operator execution
private void ResetCalculator()
{
state = 0;
accumulator = 0.0;
ResetNumerics();
TransitionState(States.State0);
}
private void ResetNumerics()
{
currentOperand = 0.0;
fractionalDivisor = 1.0;
}
}
}
using System;
namespace Calculator.Models
{
public class StateAction
{
public StateAction(Action<char> action, States transitionToState)
{
Action = action;
TransitionToState = transitionToState;
}
public States TransitionToState { get; private set; }
public Action<Char> Action { get; private set; }
}
}
I have put together a complete C#/WPF implementation of the Calculator
. There are no ‘if
’ statements or switch
statements in the implementation. I will admit that there are two null
coalescing statements, which can be argued are conditionals. Unfortunately, the C# Event functionality (which btw, I think is brilliant) does not provide a mechanism to know when an Event has been subscribed to/ unsubscribed from. And so before triggering the Event’s delegates, you do have to test if it is null
. (i.e., whether or not it has not been subscribed to).
The complete code is available to download from Github at https://github.com/SteveD430/Calculator.
Pros & Cons
Pros to Using Conditional Statements
- They match our thought processes.
- If they are not too complex, then it is easy to understand what is being tested and what action will be executed if the test is
true
. - If what is being tested is local, then it is easy to determine how the condition will be triggered.
Cons to Using Conditional Statements
The cons to using conditional statements are really summed up in the caveats attached to the last two pros – If the tests are not too complex and if what is being tested is local. The problem with conditionals is that the condition being tested may not be related to the local code. If what is being tested are conditions set-up in another section of code, possibly completely unrelated with code checking the condition, then it can be nigh impossible to understand why the program is in the state that it is. Which means, when something goes wrong, it can be extremely difficult to work out why it went wrong. You can find yourself in that classic situation where the only information is “The Computer – It say No”.
I am sure the following is a scenario we all recognize: Repeatedly, in the debugger, stepping through the same code, using same start conditions, setting up complex watch statements in a vain attempt to track down the chain of events that lead to the object or objects under test being set to the state that caused the problem. And when the problem is finally understood, assuming it ever is, it could well be that it is too complex to resolve, or resolving it at the start of the events too risky, or it may even be a valid scenario that had not previously been considered. Whatever the reason, the solution, all to often, is to “Add in another ‘if’ statement to trap the condition, thereby solving this instance of the problem“. Adding another ‘if
’ statement makes the code more complex, less maintainable, more likely to cause problems in the future, and is rarely the correct solution.
State Oriented Programming attempts to reverse the ‘if this then that
’ processes. Instead of testing the state of an object (or objects) and then conditionally executing some behaviour, SOP sets the behaviour that is needed at the point where the object or objects move(s) into the required state, The behaviour can then be executed either at the point of state transition (event driven paradigm) or injected into the code where the ‘if
’ statement would have been (procedural paradigm).
Pros to SOP
- It makes you think about state and state transitions before coding.
- It reduces code complexity by removing ‘
if
’ and other conditional statements. - It associates the behaviour change with the object state change. The state change and behaviour change are tied together code wise.
- It dramatically improves the ability to undertake TDD, because you know all possible states and how to trigger them before any coding has taken place. Plus the testing of the code is easier, because it is easier to ensure all code paths are tested.
Cons to SOP
- Not all applications lend themselves to the SOP approach. For instance, it is possible for an object to have a very large or even an infinite number of states (well as near infinite as you can get with a PC) – for example, if an objects state changes when a double property hits a certain value. Generally, Event driven programs (such as UI based apps) lend themselves nicely to the SOP approach. Algorithmic and Data mining apps far less so.
- Languages do not always have the constructs that make implementing SOP easy. The
Calculator
example above makes extensive use of pointers to functions, dictionaries, maps, events and delegates. It would be difficult to use an SOP approach without these features. - Difficult to inject the concept into existing code.
History
- 24th December, 2021: Initial version
- 15th January, 2022: Article updated