Over a million developers have joined DZone.

Adding an Overload is a Breaking Change

· Agile Zone

Reduce testing time & get feedback faster through automation. Read the Benefits of Parallel Testing, brought to you in partnership with Sauce Labs.

Adding functionality to a library, without touching the existing code should be safe for clients, shouldn’t it? Unfortunately not, adding another overload to a library can be a breaking change. It might work when the library is updated, but suddenly break when the client is recompiled – even though no code was changed. That’s nasty, isn’t it?

When working with Kentor.AuthServices I’ve had to think through versioning more in detail than I’ve done before. When is the right time to go to 1.0? What is the difference between going from 0.8.2 to 0.8.3 or 0.9.0? When researching I found that the answer to all versioning questions is Semantic Versioning.

A version is on the form Major.Minor.Patch.

  • Major is increased if there are breaking changes. The other numbers are reset.
  • Minor is increased if there is added functionality that is non breaking. The patch number is reset.
  • Patch is increased for backwards compatible bug fixes.

The terms breaking changes and backwards compatibile are keys to the definitions, so to use semantic versioning requires keeping tight control of what changes are breaking and not. In most cases it is quite simple, but there are a few pitfalls that I’ll explore in this post.

The background to this post is that I listened to Scott Meyers’ NDC talk Effective Modern C++ and was reminded on how C++ programmes have to keep track of all kinds of nastiness in the language that might turn into hard to track-down bugs. C# is a lot easier in many ways, with way fewer pitfalls, but sometimes I think we make it to simple to ourselves. C++ developers are often quite good at the language detailsbecause they have to. Being a C# developer it is possible to survive for much longer without knowing those details, but being ignorant of them will eventually result in nasty bugs. That eventuality will probably not happen on a lazy Tuesday morning with a lot of time to fix it. It will happen a late Friday afternoon, right before the important production deployment scheduled for the weekend…

As C# developers I think that we should have a look at the C++ community and see what we can learn from them. So let’s dive into some code and see how we can break things.

The Baseline Code

Let me introduce version 1 of my sample library.

public static class Utility
{
  public static void Method(object obj)
  {
    Console.WriteLine("Utility.Method(object)");
  }  
 
  public static void DefaultMethod(int i = 7)
  {
    Console.WriteLine("Utility.DefaultMethod({0})", i);
  }
}

There is also a small client program that uses the library.

public static class Program
{
  public static void Main(string[] args)
  {
    Utility.Method(17);
    Utility.DefaultMethod();
  }
}

When built and run, the program outputs the expected values.

Utility.Method(object)
Utility.DefaultMethod(7)

Updating the Library

Later on, the library is updated.

public static class Utility
{
  public static void Method(object obj)
  {
    Console.WriteLine("Utility.Method(object)");
  }  
 
  public static void Method(int i)
  {
    Console.WriteLine("Utility.Method(int)");
  }
 
  public static void DefaultMethod(int i = 42)
  {
      Console.WriteLine("Utility.DefaultMethod({0})", i);
  }
}

A new overload is added and the default value of the parameter to DefaultMethod is changed. This version is compatible with the old one, so the library dll can be changed without rebuilding the client application. When run, the client application produces the following output.

Utility.Method(object)
Utility.DefaultMethod(7)

It’s exactly the same as before. Updating the library to a new version is not affecting the output of the program. But we’ve unintentionally put ourselves in a very unstable situation.

Rebuilding the Client

Imagine that you have just taken over the maintenance of this program. You have a production installation that is working. You have the source that the program was built from and you have the download link to the latest version of the library. Everything looks in order, so you start setting up your development environment, downloads the library and builds the code. But it’s not producing the same results. Whatever you try is in vain. You can’t build the code and produce the same results. The state of the system when you took over was unstable and cannot easily be reproduced.

What has happened is that an apparently innocent rebuild of identical source code changes the behaviour of the program. If rebuilt, the program now produces the following output.

Utility.Method(int)
Utility.DefaultMethod(42)

A different overload is called and the default value is changed, even though the source code is identical.

The problem is in the compilation and linking against the external library of different versions. The overload resolution and default value handling is managed at compile time and uses the information of the external library version used at the time of compilationCompiling against a different version gives different results; even though running running with a later version of the library doesn’t.

Compile Time Library Linking

It is during compilation that the overload resolution takes place. It checks the available overloads in the library and then hard wires that selection into the compiled output. It doesn’t matter that the run-time version used offers other overloads, the compiled program knows what overload it should call. It also knows what type conversions need to be done to be able to call that overload. In this case, the int will have to be boxed into an object.

The same is true for the default value. Even though the default value is listed along with the utility method, it isn’t handled that way by the compiler. This is how the compiler reasons.

The program is calling Utility.DefaultMethod() but it’s missing an argument. That’s a syntax error. Ha! I can blow the compilation up in pieces with a syntax error! No, wait, there’s something in the method declaration. Ahh a default value. Ok, I’ll take it. I’ll change the call toUtility.DefaultMethod(7) and then I can compile it.

I’ve always suspected that compilers really like blowing up the compilation with syntax error messages and now I have it confirmed.

The important thing however is that the compiler is effectively changing the call to hard wire the default value into the call site. Even though the compiled library is replaced to one with a new default value that doesn’t matter as the default value is extracted during compilation.

Breaking Changes

Ok, now we’ve seen that overload resolution and default values can turn seemingly innocent changes to dependency nightmares. To avoid getting into trouble there are three simple practices that will save the day.

  1. New overloads and changed default values are breaking changes to library compatibility and requires a new major version.
  2. Using default values should be avoided in public facing APIs.
  3. Always make a complete rebuild and test it before deploying when dependencies are updated. This avoids the scenario above where a rebuild without code changes alters the behaviour of the program.

The complete code for this post is available on github. Run the test.bat file to compile and run the different code versions in the same order as used in this post.

The Agile Zone is brought to you in partnership with Sauce Labs. Discover how to optimize your DevOps workflows with our cloud-based automated testing infrastructure.

Topics:

Published at DZone with permission of Anders Abel, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}