Ayende @ Rahien

Refunds available at head office

API Design

There are several important concerns that needs to be taken into account when designing an API. Clarity is an important concern, of course, but the responsibilities of the users and implementers of the API should be given a lot of consideration. Let us take a look at a couple of designs for a simple notification observer. We need to observe a set of actions (with context). I don't want to have force mutable state on the users, so I have started with this approach (using out parameters instead of return values in order to name the parameter):

public interface INotificationObserver
{
    void OnNewSession(out object sessionTag);
    void OnNewStatement(object sessionTag, StatementInformation statementInformation, out object statementTag);
    void OnNewAction(object statementTag, ActionInformation actionInformation);
}

I don't really like this, too much magic objects here, and too much work for the client. We can do it in a slightly different way, however:

public delegate void OnNewAction(ActionInformation actionInformation);

public delegate void OnNewStatement(StatementInformation statementInformation, out OnNewAction onNewAction);

public interface INotificationObserver
{
    void OnNewSession(out OnNewStatement onNewStatement);
}

Comments

Frederik
10/11/2008 07:31 AM by
Frederik

What's exactly the point in not using events here?

You could easily create three events: NewSessionCreated, NewStatementCreated and NewActionInvoked. Then any client can use an arbitrary subset of these events to implement his desired behaviour...

I have to admit that I never understood the need for the observation pattern in c#, for me it's integrated in the language.

Andrey Shchekin
10/11/2008 09:40 AM by
Andrey Shchekin

Your solution is tailored for a specific use case that you envision, and that specific use case is probably easier done your way. However, the user will also have to understand and conform to your use case, which may be a problem.

I agree with Frederik:

event EventHandler <sessioneventargs NewSession;

event EventHandler <statementeventargs NewStatement;

event EventHandler <actioneventargs NewAction;

// names can be better, but I do not know what they actually do

So you can handle all actions without even thinking on what a session is. If you want to handle them in a specific way for a specific statement, you can have NewAction event on the StatementContext available through the StatementEventArgs.

Andrey Shchekin
10/11/2008 09:41 AM by
Andrey Shchekin

Sorry, the blog ate my generics.

event EventHandler NewSession;

event EventHandler NewStatement;

event EventHandler NewAction;

Ayende Rahien
10/11/2008 11:37 AM by
Ayende Rahien

Frederik & Andrey,

The problem is state. I don't want to just just any old NewStatement, I want to call NewStatement for a particular session

Frederik
10/11/2008 01:33 PM by
Frederik

And which state exactly can't you add to the event as EventArgs?

shovavnik
10/14/2008 10:59 PM by
shovavnik

The general pattern for using EventArgs is to pass the "state" data to the constructor and refer to that data using read only properties.

This effectively makes EventArgs (and its properly designed descendants) immutable and therefore, I think, suitable for the scenarios you're envisioning.

There are, of course, some exceptions to this rule (namely, CancelEventArgs), but they are few and far between, target a different use case and do not inhibit using EventArgs correctly otherwise.

You don't have to use events and EventArgs; you can use a similar pattern if you want, as long as you pass around an immutable state-holding object.

On the other hand, using out arguments in the API forces the programmer to create variables that clutter up the code and may not be needed to begin with.

They also make the code difficult to maintain when (not if) the API changes because there's no encapsulation, as with EventArgs.

Another disadvantage is that they inhibit fluent programming.

In my experience, out arguments are either used in "Try" kind of statements for safe conversions or other scenarios where exceptions are ignored, or in opaque difficult-to-document and non-standard scenarios. While your example specifically does not seem to fall into either category, usually they do, and probably should not be recommended as a programming style.

Anyway, that's my take. I'd be happy to hear a contrary opinion...

Andrey Shchekin
10/15/2008 07:08 AM by
Andrey Shchekin

__I don't want to just just any old NewStatement, I want to call NewStatement for a particular session.

That's easy, just add NewStatement event to a SessionContext provided in a SessionEventArgs.

Ayende Rahien
10/15/2008 07:19 AM by
Ayende Rahien

Yes, I can.

However, I find the usage of the out parameter make it explicit that I have to fill in the value, and make the purpose of the code clearer.

It is actually a delegate which has another out parameter for a deeper layer in the code, which also maintains state. Seems simpler that way to me

Andrey Shchekin
10/15/2008 07:52 PM by
Andrey Shchekin

Do you __have to subscribe to all of them?

What if you do not care about Actions?

It probably depends on what observer is for.

Ayende Rahien
10/15/2008 09:01 PM by
Ayende Rahien

I my scenario, yes I do. There is a well defined contract between the observer and the observee.

Comments have been closed on this topic.