Udostępnij za pośrednictwem


Making callbacks more explicit

Recall my previous post Events and Callbacks vs. Explicit Calls that outlined pros and cons of both (events and callbacks on one side and explicit calls on the other side) approaches.

Explicit calls imply that callee plays certain role from caller’s perspective thus making the relationship between the two explicit as well. Consider simple undo/redo example:

 interface ICommand
{
    // Not every command can be undone. Returns true if it can be undone,
    // false - otherwise.
    bool CanUndo { get; }
    // Executes the command.
    void Do();
    // Reverts changes. NotSupportedException must be thrown in case 
    // command cannot be undone.
    void UnDo();
}

// Provides undo/redo mechanism.
class History
{
    // Makes supplied command part of the history.
    public void Remember(ICommand cmd)
    {
        // implementation is not relevant.
    }

    // other members elided.
}

Caller (History) clearly communicates its expectations by requiring callee (particular command) to conform with command’s contract (indivisible logical unit).

While being explicit makes code more clear it has its price. It requires consumers to create new types that conform to contract. It is not a problem if the type created will be reused by other parts of the application or it has complex logic. But it is also common that it may have trivial logic that is used only in caller (undo/redo mechanism in this case) scenario. In this case creating a new type sounds like an overhead.

I whish C# has capabilities (“Object Expressions”) similar to F# where I can implement interface “in place” like this:

 let cmd = 
    { 
        new ICommand with 
            member x.CanUndo 
                with get() = false
            member x.Do() = Console.Write("Done") 
            member x.UnDo() = raise (new NotSupportedException())
    }

Although we can provide default implementation that uses delegates.

 class ActionCommand : ICommand
{
    private readonly Action m_do;
    private readonly Action m_undo;

    public ActionCommand(Action doCallback, Action undoCallback)
    {
        if (doCallback == null)
        {
            throw new ArgumentNullException("doCallback");
        }
        m_do = doCallback;
        m_undo = undoCallback;
    }

    public bool CanUndo
    {
        get { return m_undo != null; }
    }

    public void Do()
    {
        m_do();
    }

    public void UnDo()
    {
        if (!CanUndo)
        {
            throw new NotSupportedException();
        }
        m_undo();
    }
}

While conforming to contract ActionCommand eases creation of lightweight scenario dedicated implementations “in place” avoiding types proliferation. But still the type must be discovered first by developers. It is negligible effort but it is still nonzero. In order to level this effort let the original consuming code do the job.

 // Provides undo/redo mechanism
class History
{
    // Makes supplied command part of the history
    public void Remember(ICommand cmd)
    {
        // implementation is not relevant
    }

    // Makes command represented by pair of callbacks part of the history
    public void Remember(Action doCallback, Action undoCallback)
    {
        Remember(new ActionCommand(doCallback, undoCallback));
    }

    // other members elided
}

You should not put every possible “shortcut” into overloads but only the most commonly used one.

What benefits does this approach has? It benefits from being close to explicitly defined role making it easier to understand and use callback based API that is useful in case of lightweight single scenario use implementations.

Summary:

  • CONSIDER creating callback based overload next to overload that consumes corresponding abstraction