9/2/07

A usage scenario for empty "marker" interfaces

There is a well-known advice (originating probably from the FDG book) to avoid interfaces with no members. Such interfaces are mostly used to mark types, and testing if a type is marked is done with the "is" operator like this:

if (myObject is INamespaceLevel)

It is being offered to use attributes instead, for example, like this:

if (!obj.GetType().IsDefined(
typeof(ObsoleteAttribute), false))
...

The advantages of attributes are:
  1. they can have parameters
  2. you can precisely control how attributes are inherited. You can easily turn the attribute off on a derived type, whereas you can't erase an interface from a derived type's inheritance tree, if a base type already implements it.

The disadvantages of attributes are:
  1. the clumsy syntax
  2. and the runtime costs of checking (reflection is slower than the is operator).

A possible usage scenario for marker interfaces
I had one situation so far where marker interfaces seem to be quite useful and look neat. Frankly, as I write this, I realize that I could have taken attributes as well, but I already wrote too much of a post so it's a pity to throw it away. When I started this post, I was a strong believer that marker interfaces are good, now I think attributes deserve a chance as well :-)

Anyway, here's the example that I originally wanted to post (and you judge by yourself if marker interfaces are justified here). In the C# code editor I am building, language constructs were modeled by types inheriting from the Block class, for example, like this (a small subtree of the entire AST):



Now, some blocks are allowed to be nested in other blocks. For example, a class can be nested within a namespace or another class, and a method with a body can be nested in a class or a struct. To determine, where a language construct can be used, I introduced a set of marker interfaces:

public interface INamespaceLevel { }
public interface IClassLevel { }
public interface IMethodLevel { }

Now, when we drag-and-drop or copy-paste blocks, determining if a block can be dropped within a container is easy. Each container has a list of allowed interfaces that can be accepted (not necessarily a single interface, we want to be flexible). Once we drag a block over the container, we look if the dragged block implements any of the interfaces we can accept:

bool foundAssignable = false;
foreach (Type acceptableType in AcceptableBlockTypes)
{
if (acceptableType.IsAssignableFrom(dragged.GetType()))
{
foundAssignable = true;
}
}

We fill the AcceptableBlockTypes list like this:

AddAcceptableBlockTypes<IClassLevel>();

And here's the definition for AddAcceptableBlockTypes:

private Set<Type> AcceptableBlockTypes = new Set<Type>();

public void AddAcceptableBlockTypes(params Type[] acceptableBlockTypes)
{
foreach (Type t in acceptableBlockTypes)
{
if (!AcceptableBlockTypes.Contains(t))
AcceptableBlockTypes.Add(t);
}
}

public void AddAcceptableBlockTypes<T1>()
{
AddAcceptableBlockTypes(typeof(T1));
}

public void AddAcceptableBlockTypes<T1, T2>()
{
AddAcceptableBlockTypes(typeof(T1), typeof(T2));
}

public void AddAcceptableBlockTypes<T1, T2, T3>()
{
AddAcceptableBlockTypes(typeof(T1), typeof(T2), typeof(T3));
}

Now I wonder how sane this is and if I should really have taken attributes instead. I like the usability of the current API and it looks like the approach works fine for my editor. Now let's wait and see how it scales as I modernize the editor to support more recent C# versions than 1.0 :) I'll keep you posted.

No comments: