The Architect´s Napkin

Software Architecture on the Back of a Napkin
posts - 69 , comments - 227 , trackbacks - 0

My Links

News

Archives

Post Categories

Designing on different levels of abstractions with Event-Based Components

One of the biggest problems with object oriented designs is its inability to express architectures on different levels of abstractions. True, there are Packet Diagram, Component Diagram, and Class Diagram in UML, which are geared towards different levels of abstraction. But I ask you: How do you translate them into code? Except for Class Diagrams that´s not so easy. There are hardly any guidelines – at least when it comes to the .NET platform. What´s a component in .NET terms? What´s a packet?

And even if there are translations for those diagrams why is it necessary to have different ones for different levels of abstractions? All these diagrams are at their core just dependency diagrams. So why switch gears when zooming through the abstraction levels of an architecture?

On top of that I find dependency diagrams of little value when designing software. At least after starting to work with Event-Based Components they have lost much of their usefulness. Dealing with dependencies is overrated ;-) Or let me say: Don´t focus on dependencies. Don´t let them blind you for the more important aspects of your application: the processes.

To show you a different way of designing applications, let me build on what I showed you in my previous article. Remember I modelled a trivial application using activities. I started with verbs instead of nouns. And when I translated those activities into classes I let them communicate with events.

Today I want to move on with Event-Based Components (EBC) by tackling a slightly more difficult problem where it becomes useful to think on different levels of abstraction. Here it is:

I´d like a small desktop calculator application. Like the one Windows offers.

  • I want to be able to enter real numbers
  • I want to operate on them with +, –, *, /, and ^
  • I want to discard the current number
  • I want to reset the whole calculation

Here´s how it could look like:

image

Not a very creative visual design, I know :-) But it serves the purpose.

How would the usual object oriented developer tackle this problem? I can hardly imagine that anymore ;-) I´ve become so used to EBC architectures. But you could try to find a solution along your habits before you read on.

From feature to process

Here´s how I´d approach the requirements. I´d decompose it into distinct features, i.e. small slices of end-to-end functionality. For this scenario that´s easy, because it´s simple and the requirements already state the features. But for larger applications that´s a quite creative task taking up some time. And it´s task to be undertaken not only by you! In fact the product owner is the main person responsible for that.

Anyway… for each feature I then design a process. This could be called the feature process, i.e. I determine the main steps leading from trigger to result. Here´s the feature process for entering/editing the number:

image

Some source – the frontend – fires an event for each character related to numbers (“0”..”9”, “,”) and the activity uses it to build-up the current number displayed.

We could start building the application with just this design. It would already offer some value to some imagined customer. Shall we? No, let´s quickly add the design for a second feature related to numbers:

image

This was pretty obvious, right? You could have come up with that after what I´ve shown you in my previous article.

In order to implement this design we now need to think about how to translate it to classes. I suggest the following:

image

It´s two classes again. This time, though, I´d like to show you a different way of translating the activities to methods:

class NumberAggregator
{
    private double currentNumber;
    private double numberFactor;
    private double digitFactor;


    public NumberAggregator()
    {
        Reset();
    }


    public void ExtendNumber(char c, Action<double> out_currentNumber)
    {
        if (c == '.')
        {
            if (this.digitFactor >= 1.0)
            {
                this.numberFactor = 1.0;
                this.digitFactor = 0.1;
            }
        }
        else
        {
            this.currentNumber = this.currentNumber*this.numberFactor;
            this.currentNumber += int.Parse(c.ToString()) * this.digitFactor;
            if (this.digitFactor < 1.0) this.digitFactor *= 0.1;
        }

        out_currentNumber(this.currentNumber);
    }


    public void DiscardNumber(Action<double> out_currentNumber)
    {
        Reset();
        out_currentNumber(this.currentNumber);
    }

       
    private void Reset()
    {
        this.currentNumber = 0.0;
        this.numberFactor = 10.0;
        this.digitFactor = 1.0;
    }
}

Check out the signatures of the methods, e.g.

     public void ExtendNumber(char c, Action<double> out_currentNumber)

Instead of defining an event on the class as I did in the previous article, I now pass in an event handler to the activity method. This makes it self contained. It allows for using a NumberAggregator object in different contexts.

Wiring up the components then looks like this:

static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    var wc = new WinCalculator();
    var na = new NumberAggregator();

    wc.Out_NumberCharacter += c => na.ExtendNumber(c, wc.In_CurrentNumber);
    wc.Out_Clear += () => na.DiscardNumber(wc.In_CurrentNumber);

    Application.Run(wc);
}

This way it´s clear that ExtendNumber produces a result which is sent to In_CurrentNumber. I find this a more literal translation of the EBC diagram.

The calculator dialog window implementation is simple. Have a look at the sources, if you like.

image 

Zooming in

The next feature in our backlog is the actual calculation. Although an imagined customer has already gotten some value for her money, the real purpose is to apply mathematical operations to the numbers entered.

image

Calculating the result of a mathematical operation is simple when viewed from afar: pass in the current operation and receive the result. And if that´s our view, then we want to be able to model our solution accordingly. We want the model to express our understanding.

However, we want to then zoom in and model any details we see once we start thinking more closely about the solution. How does the Calculate activity actually work? This is my idea of its design:

image

The operation triggers the read out of the current number. Then operation and current number are somehow used for the calculation which passes on the result. So what is just one activity on a higher level of abstraction (Calculate) is several activities on a lower level of abstraction (Eject current number, Apply operation, plus standard activities like Join and Split).

This is the beauty of EBC: The same meta model can be applied on all levels of abstraction. You can go as far down as you like. You start with just one activity representing the whole process… and then you decompose it into simpler activities which you again can decompose into even smaller sub-activities etc.

Communication on each level of abstraction is the same: events procossed by event handlers.

Translation on each level of abstraction is the same: use classes or methods to implement activities.

But before you try it yourself have a closer look at the diagrams on the different levels of abstraction. There is a CalculationEngine on both of them mentioned. The Calculate activity belongs to the CalculationEngine as well as the Apply operation activity. That seems strange. Should one class (CalculationEngine) serve on two levels of abstraction? No, that´s not a good idea.

Still, though, Calculate and Apply operation somehow belong closer together than, say, Apply operation and Eject current number. What can we do to express that?

How about using not only classes/methods but also namespaces during translation? We could, for example, translate Calculate to a class in namespace calculationengine. And Apply operation becomes a method on class CalculationEngine in namespace calculationengine. How´s that?

Here´s the code for Apply operation:

namespace wincalc.calculationengine
{
    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)
        {
            if (this.operation == '=') this.operands.Clear();

            this.operands.Push(input.Item2);
            var result = Calculate();

            this.operation = input.Item1;

            out_result(result);
        }
...

Pretty straightforward, I´d say. The same goes for Eject current number:

class NumberAggregator
{
    ...

    public void EjectNumber(Action<double> out_currentNumber)
    {
        out_currentNumber(this.currentNumber);
        Reset();
    }
...

The changes to the main windows are even simpler.

image

But what about Calculate? It´s a different activity compared to the other ones. The difference lies in that it does not do anything except wire up activities. It´s sole purpose is to aggregate sub-activities. In that it resembles an electronic circuit board.

I deem this a clear separation of concerns, though. A feature process is a tree of activities. And only leaf activities contain intricate domain logic. And aggregating activities, any composite process steps on higher levels of abstraction are dead simple. They only connect other activities – on which they in fact depend. See here the Calculate class for what I mean by that:

namespace wincalc.calculationengine
{
    class Calculate
    {
        public Calculate(Action<Action<double>> ejectNumber,
                         Action<Tuple<char, double>, Action<double>> applyOperation)
        {
            var opToSignal = new DropData<char>();
            var joinOpAndNumber = new Join<char, double>();

            this.In_Process = (c) =>
                {
                    opToSignal.In_Drop(c);
                    joinOpAndNumber.Input0(c);
                };

            opToSignal.Out_Signal += () => ejectNumber(joinOpAndNumber.Input1);
            joinOpAndNumber.Output += _ => applyOperation(_, this.Out_Result);
        }


        public Action<char> In_Process;

        public event Action<double> Out_Result;
    }
}

It gets injected the sub-activities (and instanciates some standard EBC pattern activities in addition) and wires them up. Since the whole class stands for just one activity I made the output an event of its own.

From the outside you cannot really distinguish between an atomic activity and a composite activity (except that atomic activities can be methods and composite ones not). They are all plugged together the same way:

static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    var wc = new WinCalculator();
    var na = new NumberAggregator();
    var calcEng = new calculationengine.CalculationEngine();
    var calc = new calculationengine.Calculate(na.EjectNumber, calcEng.In_ApplyOperation);

    wc.Out_NumberCharacter += c => na.ExtendNumber(c, wc.In_CurrentNumber);
           
    wc.Out_OperationCharacter += c => calc.In_Process(c);
    calc.Out_Result += wc.In_CurrentNumber;

    wc.Out_Clear += () => na.DiscardNumber(wc.In_CurrentNumber);

    Application.Run(wc);
}

Here´s how I organized the code in Visual Studio. Note how I put the functionality related to the calculation in a project folder. This is to emphasize the nesting visible in the design. The ebc.patterns folder contains two helper activities often needed in EBC architectures.

image 

Summary

Do you start to feel how EBC differ from object orientation? Because designing software on different levels of abstraction and then manifesting all those levels in code in a systematic way is very different from common object orientation.

Next time I´m gonna show you more of that – and probably will add some aspect orientation just for the fun of it ;-)

PS: If you think the code actually differs from the design you´re right. I replaced the Split activity with a DropData activity. So that part of the Calculate details actually looks like this:

image

PPS: I know, a feature is missing (reset whole calculation) and the calculator is not working correctly in a case like this: 1 + 2 = * 4 =. Stay tuned. I´ll fix that – sooner or later ;-)

Print | posted on Monday, July 26, 2010 9:02 AM | Filed Under [ Event-Based Components ]

Feedback

Gravatar

# re: Designing on different levels of abstractions with Event-Based Components

Very fascinating post! I also just read through your German post that had the restaurant example in it. I particularly liked that one because it showcases the power of programming to events better than the calculator here. (I mean there aren't that many ways to logically divide up the calculator's events, are there?) Over the past 18 months, my team has been moving away from "spaghetti code" and towards domain models. We're currently settled on a command driven architecture, but I really like this EBC concept.

I might expand on what you've done here by replacing all the delegates with first class events (like an event aggregator pattern, or http://www.udidahan.com/2009/06/14/domain-events-salvation/). So I'd do something like Events.Raise(new CharacterKeyPressed(char)) in place of the Action<char>. It's just more explicit in my opinion. Also, it would allow you to define conventions for wiring up events and handlers with looser coupling in the application. Have you thought about going this direction? (I realize this is probably just an introductory post.)

Another thought off the top of my head is, this would be really cool to use with the Reactive library. Particularly interesting would be finding a way to replace Calculate.Calculate() with a zipped observable event.

Anyway, I'll stop thinking out loud and probably collect my thoughts in a post later this week. I'm interested in what else you've done on this topic. -Ryan
7/27/2010 7:12 AM | Ryan
Gravatar

# re: Designing on different levels of abstractions with Event-Based Components

@Ryan: Using char as the data type is just because I do not want to clutter up my posts with too many aspects.

As you´ll see in future posts I´m all for representing the Ubiquitous Lanaguage not only in activtity names and classes/components, but also in the types of the data flowing.
7/28/2010 1:59 PM | Ralf Westphal
Post A Comment
Title:
Name:
Email:
Comment:
Verification:
 

Powered by: