Avoiding Invasive API design

time to read 3 min | 518 words

I have a strong dislike for invasive API and framework. I dislike them because they require a lot of work, avoid existing options, limit my ability to work with them, etc. They also tend to tie you to a platform for very little reason.

Here is a simple (and fictional) example. Let us assume that we want to write a library to expose our domain model in a version way. So we can do something like this:

svn://localhost/Customers/15[@revision=3]

Now, in order to handle this scenario, I have devised the following schema. I can create the end point using the following approach:

public class Customers : Versioned<Customer> { }

And I can can specify the revision using an attribute:

public class Customer
{
	public virtual int Id { get; set; }

	[SvnVersion]
	public virtual int Version { get; set; }
}

Very simple, right? And very straightforward. Except that this in incredibly invasive approach to take.

I really dislike API that work this way. Now, if I want to expose my domain model using SVN, I need to go into the domain model and start messing with that, just because I want to add another accessibility option?

This is bad. It violates the open close principal, single responsibility principal and in Ayende Annoyance Principal.

Let us try to make this easier, shall we?

public class Customers : Versioned<Customer, CustomerVersionPropertyProvider>
{
}

Now I don't have to modify the Customer entity, I can do this in a way that doesn't modify Customer, I keep things separate and in general I am happy. From the Versioned class perspective, I now define it as:

public class Versioned<TEntity, TVersionProvider>
	where TVersionProvider : IVersionProvider, new()
{
}

Actually, this is not really good. We probably want to control the creation of the version provider ourselves (it may need external dependencies, we may want to control its lifetime, etc), so we get to this design:

public class Versioned<TEntity, TVersionProviderFactory>
where TVersionProviderFactory: IVersionProviderFactory, new()
{
} public interface IVersionProviderFactory { IVersionProvider Create(); } public class Customers : Versioned<Customers, CustomersVerionProviderFactory> { }

And now we really have non invasive API, which plays well with other tools.

Except, the simple scenario that we had before turn out to be really complex. Instead of putting a simple attribute in place, we now need to implement two interfaces, do it per class, and in generally add a lot more work into our life.

That is also bad. But we can resolve that easily, by defaulting to the invasive behavior (which is really easy to explain), yet allowing extensibility. The way to do it is simple:

public class Versioned<TEntity, TVersionProviderFactory>
	where TVersionProviderFactory: IVersionProviderFactory, new() 
{
} 

public class Versioned<TEntity> : Versioned<TEntity, SvnVersionAttributeVersionProviderFactory>
{
} 


public class Customers : Versioned<Customer>
{
}

Now we have the same API for the simple (and invasive) scenario, but with minimal effort, we have made it possible to use non invasive and smart approaches.