Ralf Westphal Archiv

The Architect's Napkin

Designing on different levels of abstractions with Event-Based Components

26. Juli 2010Erstveröffentlichung: geekswithblogs.netOriginaladresseralfw.de/archiv/2010/designing-on-different-levels-of-abstractions-with-ebc/

Designing software on different levels of abstraction is nothing new. That´s what UML Packet-, Component-, and Class diagrams are for. But how does that work with Event-Based Components (EBC)? Let me show you using the small desktop calculator example from my previous posts.

The application

The calculator application should support entering numbers, and applying the four basic arithmetic operations (+, -, *, /) to them, ending with a "=" to see the result.

Starting simple

Let´s start with a very simple feature: entering/editing a number. What are the events (triggers) leading to this feature doing its job? And what´s the result of the feature?

The trigger is a digit key or the decimal point key being pressed. The result is the number to be shown updated on the display.

This translates into a functional unit (FU), i.e. a class, like this:

class NumberAggregator
{
    private double currentNumber = 0.0;
    private bool isFraction = false;
    private double fractionDivisor = 1.0;

    public void ExtendNumber(char c, Action<double> out_currentNumber)
    {
        if (c == '.')
        {
            this.isFraction = true;
        }
        else
        {
            var digit = double.Parse(c.ToString());
            if (!this.isFraction)
            {
                this.currentNumber = this.currentNumber * 10 + digit;
            }
            else
            {
                this.fractionDivisor *= 10;
                this.currentNumber += digit / this.fractionDivisor;
            }
        }

        out_currentNumber(this.currentNumber);
    }

    public void DiscardNumber(Action<double> out_currentNumber)
    {
        this.currentNumber = 0.0;
        this.isFraction = false;
        this.fractionDivisor = 1.0;

        out_currentNumber(this.currentNumber);
    }
}

Note how the class does not use events for its output, but instead takes an Action<double> out_currentNumber parameter. That´s a valid alternative for functional units whose output is directly correlated with a single input, i.e. which are not really asynchronous or need to fan out their output.

Zooming in on Calculate

Now let´s look at the more interesting part: what happens when an operator key or "=" is pressed. On a high level this is just a single activity: Calculate. It takes the pending operation and operands and produces a result.

But if I look closer, Calculate really consists of two sub-activities: first the currently entered number needs to be "ejected" from the number aggregator (and the aggregator reset), then the operation needs to be applied to the operands accumulated so far.

So I "zoom in" on Calculate and depict it as a flow of its own:

  • Eject current number
  • Apply operation

To express this nesting in code, I introduce a namespace for the sub-activities, e.g. wincalc.calculationengine, mirroring the diagram structure. This way the code structure resembles the design structure across abstraction levels.

Here´s the code for the CalculationEngine, which realizes "Apply operation":

namespace wincalc.calculationengine
{
    public class CalculationEngine
    {
        private readonly Stack<double> operands = new Stack<double>();
        private char operation = '=';

        public void In_ApplyOperation(Tuple<char, double> input, Action<double> out_result)
        {
            this.operands.Push(input.Item2);
            var result = Calculate();
            this.operation = input.Item1;

            out_result(result);
        }

        private double Calculate()
        {
            var calc = new Calculate();
            return calc.Execute(this.operation, this.operands);
        }
    }
}

And the Calculate class, using helper classes DropData<char> and Join<char, double> to combine the pending operation with the accumulated operands:

public class Calculate
{
    public double Execute(char operation, Stack<double> operands)
    {
        if (operands.Count < 2)
            return operands.Count > 0 ? operands.Peek() : 0.0;

        var op2 = operands.Pop();
        var op1 = operands.Pop();
        double result;

        switch (operation)
        {
            case '+': result = op1 + op2; break;
            case '-': result = op1 - op2; break;
            case '*': result = op1 * op2; break;
            case '/': result = op1 / op2; break;
            default: result = op2; break;
        }

        operands.Push(result);
        return result;
    }
}

Wiring it together

Finally, here´s the Main() method wiring the whole application together, connecting the GUI events to the NumberAggregator and CalculationEngine:

static void Main()
{
    var numberAggregator = new NumberAggregator();
    var calculationEngine = new wincalc.calculationengine.CalculationEngine();
    var gui = new CalculatorForm();

    gui.Out_DigitPressed += c => numberAggregator.ExtendNumber(c, gui.In_Display);
    gui.Out_OperatorPressed += op =>
        {
            double currentNumber = 0.0;
            numberAggregator.DiscardNumber(n => currentNumber = n);
            calculationEngine.In_ApplyOperation(
                Tuple.Create(op, currentNumber),
                gui.In_Display);
        };

    Application.Run(gui);
}

PS

The code above differs slightly from the diagrams: in the diagrams I originally used a Split, but in the code I replaced it with a DropData step, since only the aggregated number, not the trigger event itself, needs to flow onward.

PPS

Also note there are still known bugs in this version: the "reset whole calculation" feature is missing, and there is incorrect behavior for a sequence like 1 + 2 = * 4 =. I fixed both in a follow-up post.

← Zurück ins Archiv