Thursday, June 11, 2009

Events and Callbacks vs. Explicit Calls

From API design perspective interaction between components can be expressed in request/reply (explicit calls) or in event driven manner (events and callbacks). Consider BackgroundWorker usage example:

// BackgroundWorker executes an operation on a separate thread.
// The operation is basically an event handler of DoWork event
var worker = new BackgroundWorker();
// Once worker will be run this event will be raised in separate thread
worker.DoWork += (sender, e) =>
                {
                   // Event handler does the actual work
                   // The event is raised in a separate thread
                };
worker.RunWorkerCompleted += (sender, e) =>
                            {
                               // Performs post processing step
                               // The event is raised through sycnhronization
                               // context
                            };
// Runs worker
worker.RunWorkerAsync();

Why .NET Framework designers selected event based interaction model instead of request/reply (see hypothetical example below)?

// Represents unit of background work
internal interface IBackgroundWork
{
    // Performs actual work and will be called
    // in a separate thread
    object Do();
    // Performs post processing step and will be
    // called through sycnhronization context
    void Complete(object result);
}

internal class SomeBackgroundWork : IBackgroundWork
{
    public object Do()
    {
        // Do the actual work and return result
        return ...;
    }

    public void Complete(object result)
    {
        // Do post processing
    }
}

internal class BackgroundWorker
{
    // Runs work unit asynchronously
    public void RunWorkerAsync(IBackgroundWork work)
    {
        // elided
    }
}
var worker = new BackgroundWorker();
worker.RunWorkerAsync(new SomeBackgroundWork());

The short answer is that it is about the relationship between participants (worker and provider) and in particular the direction of the dependency. Dependency direction is identified by the domain that holds participants.

Consider IComparable<T> interface. This interface is implemented by types whose instances can be ordered. Implementing this interface on a type states that it supports comparison. On the other hand GenericComparer<T> uses IComparable<T> to compare instances.

[Serializable]
internal class GenericComparer<T> : Comparer<T>
    where T : IComparable<T>
{
    public override int Compare(T x, T y)
    {
        if (x != null)
        {
            if (y != null)
            {
                return x.CompareTo(y);
            }
            return 1;
        }
        if (y != null)
        {
            return -1;
        }
        return 0;
    }

    // othe members elided
}

Explicit calls clearly express the relationship and the consequences of the action but it limits extensibility to compile time and puts more work on API consumers because it requires them to create new types. In the "explicit" background worker example consumer will need to create new type for its unit of work. And it is highly likely that it won't be used in other places.

Consider ThreadPool class. It provides ability to execute arbitrary code on a separate thread. ThreadPool is not interested to know its consumers but it is the consumers who depend on ThreadPool to perform a task. It uses callbacks mechanism to execute arbitrary code within thread pool.

ThreadPool.QueueUserWorkItem(state =>
                            {
                             // Does actual work
                            });

Events and callbacks do not create explicit dependency and thus allows for runtime customization. Events has great tool support. It puts less work on consumers because it doesn’t require to create new types. But these benefits come at a price. It is hard to understand what happens just from looking at the code - in order to understand it all subscriptions to events must be seen. The event triggering may result in an events cascade where handling code raises next event thus creating a chain of events. From performance perspective events and callbacks are more heavyweight than explicit calls.

Summary:

  • CONSIDER using explicit calls to express the relationship (consuming several events or callbacks to perform single task may be a sign that there is role to discover)
  • CONSIDER using explicit calls if there is a potential to reuse called code in other use cases
  • CONSIDER using explicit calls to increase testability
  • AVOID expressing complex interaction using events or callbacks which will be really hard to understand
  • CONSIDER using events and callbacks to allow runtime customization and avoid undesired dependency

No comments: