Use Extension Methods to Phase Out a Bad API

I’m working on a project for a customer where we are trying to get a new version of a product out the door. We need to add some features to an existing API, but don’t want to break the existing API for existing customers. Common story, common place to be.

However, in adding these new features, we really need to be modifying an existing API to add new options to existing methods. But we find ourselves in a place I’ve seen a number of times: the API tried to be too granular about its arguments. It provided flexibility for optional arguments with overloaded methods. Not a bad thing from an OO perspective. The slippery slope is trying to anticipate how many arguments you might actually need to that method over time and which of those arguments might be optional.

Defining a Clean, Version Tolerant API

Say you start with a simple API like this:

publicinterface ISomeInterface { void SomeMethod(double alwaysNeeded); void SomeMethod(double alwaysNeeded, int optionArg1); void SomeMethod(double alwaysNeeded, string optionArg2); void SomeMethod(double alwaysNeeded, int optionArg1, string optionArg2); }

There is one always needed argument, and a couple of optional args. Fine when there are just two options. What about when the list grows to 3, 4, 5, 6? How many overloads do you have to add to cover all the reasonable combinations of those arguments a user might want? The size of your API goes exponential, and the usability of it goes downhill quick. This is the situation we find ourselves in. And this is why I have long recommended that unless an API is going to be static and always take the same set of arguments, you are better off packaging up those arguments in a single complex argument – a data structure single argument such as the following: publicclass MyMethodArg { publicdouble AlwaysNeeded { get; set; } publicint OptionalArg1 { get; set; } publicstring OptionalArg2 { get; set; } } Then you can simplify the interface definition to just the following: publicinterface ISomeInterface { void SomeMethod(MyMethodArg arg); } Now, over time, you can add additional required or optional arguments without affecting the interface API at all. As long as you write graceful handling for when optional parameters are not set explicitly on the data structure, you have much better ability to change the underlying arguments over time without breaking existing clients. And you have a much cleaner, easier to understand API. #### Extension Methods to the Rescue So then the question becomes: If you find yourself with an API that looks like the original version and you want to move to the cleaner version immediately above, do you have to break all your existing clients to get there? Thanks to extension methods, the answer is no. Say you have existing clients that have code that depends on the original API. A simple one might look like the following console app: class Program { staticvoid Main(string[] args) { ISomeInterface itf = new SomeImplementation(); // V1 methods - obsolete itf.SomeMethod(1.0); itf.SomeMethod(1.0,42); itf.SomeMethod(1.0,"foo"); itf.SomeMethod(1.0, 42,"foo"); // V2 method itf.SomeMethod(new MyMethodArg { OptionalArg1 = 42 }); Console.WriteLine("Press Enter to Exit..."); Console.ReadLine(); } }

For sample purposes, say the original implementation looked like the following:

publicclass SomeImplementation : ISomeInterface { void ISomeInterface.SomeMethod(double alwaysNeeded) { Debug.WriteLine("SomeMethod no optional args"); } void ISomeInterface.SomeMethod(double alwaysNeeded, int arg) { Debug.WriteLine("SomeMethod optional int arg"); } void ISomeInterface.SomeMethod(double alwaysNeeded, string arg) { Debug.WriteLine("SomeMethod optional string arg"); } void ISomeInterface.SomeMethod(double alwaysNeeded, int arg1, string arg2) { Debug.WriteLine("SomeMethod optional int and string args"); } }

What you can do is use Extension methods to move the old API off the primary interface you want to keep for the long term (assuming there are also other methods and properties on that interface that you want to remain there instead of just defining a new interface with the new API). You could add the following class to your existing project:

publicstaticclass SomeInterfaceExtensions { [Obsolete] publicstaticvoid SomeMethod(this ISomeInterface itf, double alwaysNeeded) { itf.SomeMethod(new MyMethodArg { AlwaysNeeded = alwaysNeeded }); } [Obsolete] publicstaticvoid SomeMethod(this ISomeInterface itf, double alwaysNeeded, int arg) { itf.SomeMethod(new MyMethodArg { AlwaysNeeded = alwaysNeeded, OptionalArg1 = arg }); } [Obsolete] publicstaticvoid SomeMethod(this ISomeInterface itf, double alwaysNeeded, string arg) { itf.SomeMethod(new MyMethodArg { AlwaysNeeded = alwaysNeeded, OptionalArg2 = arg }); } [Obsolete] publicstaticvoid SomeMethod(this ISomeInterface itf, double alwaysNeeded, int arg1, string arg2) { itf.SomeMethod(new MyMethodArg { AlwaysNeeded = alwaysNeeded, OptionalArg1 = arg1, OptionalArg2 = arg2 }); } }

Then, the implementation class can be collapsed to the following:

publicclass SomeImplementation : ISomeInterface { void ISomeInterface.SomeMethod(MyMethodArg arg) { Debug.WriteLine("SomeMethod complex arg"); } }

The client can remained unchanged against the V1 API thanks to the extension methods, but will now get compiler warnings that the API is obsolete thanks to the Obsolete attributes. But they won’t break. New clients will not even see the old API unless they go looking for it, and then they should be smart enough not to write new code against things marked with the Obsolete attribute when there is a perfectly usable method on the primary API itself.

So this is the approach we are going to use to move forward the original lack-of-forethought API and clean it up for the long term.

Obviously this approach will only work if your clients (that you don’t want to break) are on .NET 3.5. If you could afford to lock them down to just .NET 4.0 or later, you would have another option with C# optional parameters or default values, but that is unlikely for most apps at this point in time.