Avoiding Invasive API design
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.
Comments
Nice example of doing Mixins with C# 2.0.
But now please explain the difference of
[SvnVersion]
and
[Property]
Interesting stuff.
Ayende Annoyance Principal, AAP. Gonna start using that.
@Markus: I was thinking exactly the same thing. I use attributes all over the place.
@Ayende: Would this translate for [Property] i.e. could ActiveRecord/NHibernate using attributes theoritically be re-written this way and would it even be a good idea?
I wonder if it wouldn't be easier to use IoC to inject whatever versioning provider you'd want. That way, you would not have to waste a base class, of which there can only be. So this could get a bit more complicated if you wanted to apply it to more than one feature.
(@Markus - mixins could be another solution, but I don't see how this is a way of doing mixins. Here's ours: http://www.re-motion.org/blogs/team/archive/2008/02/20/introducing-mixins-finally.aspx. You could probably even use it to provide several features like Versioned<,>, but I'd still try IoC first.)
@Stefan - I meant the following: Replace Versioned with IVersioned, so you don't need a base class and attach the functionality of Versioned by using proxies (with IoC preferably)
Makus, I'm not sure if I understand you correctly, but I think this is not something that IoC can do. If you specify IVersioned at compile-time, you also have to implement it manually. But if you're interested in mixins, do have a look at the link I posted. If you know an IoC container that enables similar stuff, I'd like to hear about it!
Stefan,
DP2 allows the creation of Interface-proxies, thus adding the interface at runtime. Although AFAIK mixins are not implemented in DP2 yet, it is possible to emulate mixins by using an interceptor that redirects calls to the proxy to either the base object or the mixin object. Since facilities can add interceptors to components without explicit configuration, it is possible to use Windsor for that task.
I'm preparing a blog post on that topic (I started it when Ayende complained about C#'s inability of creating mixins, but I don't have much time currently^h^h in general).
Adam,
There is no relation to AR. It is merely a way to demonstrate that.
NHibernate can certainly be handled this way, since is it entirely non invasive.
AR is an invasive layer that makes working with NH much easier.
Stefan,
Customers is an end point in this regard, it is not doing actual useful work on its own.
Think about it like System.Windows.Forms.Control.
Markus,
This sample has nothing to do with NH/AR.
It is simple here to make a point about invasive approaches
Ayende,
you're right, I failed to notice that Customer and Customers are two different classes. But in this case I don't understand how the attribute solution you initially discard would be invasive. After all, you wouldn't have to touch the domain class if your end point changed, would you?
I need to tell that endpoint what the revision property is.
The question is how to do it.
I can do it with an attribute (invasive) or allow non invasive approaches
Markus,
DP2 is not an IoC container. It does have an mixin API though, but it's a bit awkward to use and does not bring the full power of mixins. After all, it's more or less a byproduct of the proxy generation stuff of DP. Compare this to what you can do with C++ mixins, there's a lot more to it than just delegating interface members to other objects. (Or, again, read my link.)
I quickly read up on Windsor facilities. As far as I can tell, they are not really an IoC feature, but they might enable you to code a mixin facility. But that would probably be based on DP's stuff, and we made a decision long ago to not build our mixin stuff on DP for a reason, so you might not be able to go all the way using this approach.
Anyway, I'd like to see the code! Can you ping me on stefan dot wenig at rubicon dot eu when you've got your blog post finished?
OMG, Customer and Customers confused me again. I just read it again, the [SvnVersion] attribute is in Customer (not in the end point), so you're perfectly right - it's invasive.
But still, would that not be easier to achive using an IoC container (probably configured by code though)? Passing a factory type as a generic argument might be a bit harder to read, especially if the reader is familiar with IoC.
Speaking of IoC, it's funny how this reminds me of Spring itself: AFAIR they refuse to have invasive attributes for default classes on their type members, but rather have the user code seperate config classes if they want to configure by code. So if you'd use Spring to configure your IVersionProviderFactory, you'd be meta-uninvasive ;-)
LOL.
Yes, IoC is the way I would go.
In general, I would say that I want IVersionProvider<T> from the IoC and let it deal with it.
this is for a solution that cannot assume IoC
this just remindes me of Object Builder... the fact that just by looking at the properties of a class you can tell that application is using a particular form IoC (Object Builder) makes me very uneasy...
Could someone explain the word invasive in this context.
regards
Benny
Invasive in this context means that the usage of library/framework means that I have to modify parts of my code.
Comment preview