Abstract Base Classes Have Versioning Problems Too

code 0 comments suggest edit

This is part 2 in an ongoing series in which I talk about various design and versioning issues as they relate to Abstract Base Classes (ABC), Interfaces, and Framework design. In part 1 I discussed some ways in which ABCs are more resilient to versioning than interfaces. I haven’t covered the full story yet and will address some great points raised in the comments.

In this part, I want to point out some cases in which Abstract Base Classes fail in versioning. In my last post, I mentioned you could simply add new methods to an Abstract Base Class and not break clients. Well that’s true, it’s possible, but I didn’t emphasize that this is not true for all cases and can be risky. I was saving that for another post (aka this one).

I had been thinking about this particular scenario a while ago, but it was recently solidified in talking to a coworker today (thanks Mike!). Let’s look at the scenario. Suppose there is an abstract base class in a framework named FrameworkContextBase. The framework also provides a concrete implementation.

public abstract class FrameworkContextBase
{
  public abstract void MethodOne();
}

Somewhere else in another class in the framework, there is a method that takes in an instance of the base class and calls the method on it for whatever reason.

public void Accept(FrameworkContextBase arg)
{
  arg.MethodOne();
}

With me so far? Good. Now imagine that you, as a consumer of the Framework write a concrete implementation of FrameworkContextBase. In the next release of the framework, the framework includes a method to FrameworkContextBase like so…

public abstract class FrameworkContextBase
{
  public abstract void MethodOne();
  public virtual void MethodTwo()
  {
    throw new NotImplementedException();
  }
}

And the Accept method is updated like so…

public void Accept(FrameworkContextBase arg)
{
  arg.MethodOne();
  arg.MethodTwo();
}

Seems innocuous enough. You might even be lulled into the false sense that all is well in the world and decide to go ahead and upgrade the version of the Framework hosting your application without recompiling. Unfortunately, somewhere in your application, you pass your old implementation of the ABC to the new Accept method. Uh oh! Runtime exception!

The fix sounds easy in theory, when adding a new method to the ABC, the framework developer need to make sure it has a reasonable default implementation. In my contrived example, the default implementation throws an exception. This seems easy enough to fix. But how can you be sure the implementation is reasonable for all possible implementations of your ABC? You can’t.

This is often why you see guidelines for .NET which suggest making all methods non-virtual unless you absolutely need to. The idea is that the Framework should provide checks before and after to make sure certain invariants are not broken when calling a virtual method since we have no idea what that method will do.

As you might guess, I tend to take the approach of buyer beware. Rather than putting the weight on the Framework to make sure that virtual methods don’t do anything weird, I’d rather put the weight on the developer overriding the virtual method. At least that’s the approach we’re taking with ASP.NET MVC.

Another possible fix is to also add an associated Supports{Method} property when you add a method to an ABC. All code that calls that new method would have to check the property. For example…

public abstract class FrameworkContextBase
{
  public abstract void MethodOne();
  public virtual void MethodTwo()
  {
    throw new NotImplementedException();
  }
  public virtual bool SupportsMethodTwo {get{return false;}}
}

//Some other class in the same framework
public void Accept(FrameworkContextBase arg)
{
  arg.MethodOne();
  if(arg.SupportsMethodTwo)
  {
    arg.MethodTwo();
  }
}

But it may not be clear to you, the framework developer, what you should do when the instance doesn’t support MethodTwo. This might not be clear nor straightforward.

This post seems to contradict my last post a bit, but I don’t see it that way. As I stated all along, there is no perfect design, we are simply trying to optimize for constraints. Not only that, I should add that versioning is a hard problem. I am not fully convinced we have made all the right optimizations (so to speak) hence I am writing this series.

Coming up: More on versioning interfaces with real code examples and tradeoffs. More on why breaking changes suck. ;)

Technorati Tags: ASP.NET,ASP.NET MVC,Interfaces,Abstract Base Classes,Framework

Found a typo or error? Suggest an edit! If accepted, your contribution is listed automatically here.

Comments

avatar

14 responses

  1. Avatar for Evan
    Evan February 21st, 2008

    Are you guys planning on shipping assembly redirects for the ASP.NET extensions (MVC)?
    If not, all this commotion about not having to recompile across versions is largely moot. Upgrading a dependency from 1.1 to 1.2 is a breaking change, regardless of what the types and type members look like. The CLR enforces that for us.
    Barring that, you'd need to actually ship more than 1 assembly (where stable types are in an "interface assembly" if you will), and developers would actually have to manage their dependencies (gasp!).

  2. Avatar for Scott Bellware
    Scott Bellware February 21st, 2008

    > decide to go ahead and upgrade the version of the Framework hosting
    > your application without recompiling
    If Microsoft customers are being so negligent in application lifecycle concerns, then it would be hard not to walk the cat back to Microsoft's own traditional unwillingness to provide meaningful, ethical, and responsible guidance to the customers who still look to Microsoft for guidance.
    If Scott Gu or Soma S would just get together and post a blog saying that this kind of thing is reckless and irresponsible, and that the tools and processes to support safer means of asset management are easily and readily available (unless we're talking about heavyweight, high-ceremony stuff like TFS), then the whole Microsoft customer developer ecosystem ball might actually start to move forward in a meaningful way, with customers' interested put well before Microsoft's own interests.
    Just a thought.

  3. Avatar for Ayende Rahien
    Ayende Rahien February 21st, 2008

    In other words, now, in order to support versioning well, you have to put code in an abstract base class. That defeat the idea of using this ABC as a replacement for interfaces.
    It also means that instead of getting a pretty compiler error with all the missing methods that I need to implement, you now trade either runtime exception or, even worse, a default implementation that may break utter havoc with the implementing class.

  4. Avatar for Mark Channing
    Mark Channing February 21st, 2008

    Ayende Rahien says "It also means that instead of getting a pretty compiler error" pre-supposes you own all the code and can compile it.
    When developing a framework you try do to things which do not break client code. Therefore adding a new method to an interface you will break client code, add a new non abstract method to an ABC class and you will not. It is then the client who should decide when they wish to take advantage of the new method.
    It still leaves the original problem that some thing may pass in the original version of ABC but that is squarely in the domain of the client of the framework and not the framework developer. How that old version is still knocking about would seem a bit contrived.

  5. Avatar for Jason
    Jason February 21st, 2008

    I'm still confused about why this is such a big deal. whether you add/change a signature of an interface or an abstract class there is still a change.
    Ayende Rahien, had a post in response to part 1 which made more sense to me than this (and the previous) post. I agree, if you're building the framework, you should be responsible for compatibility. If a developer is going to upgrade, they should be responsible for managing the changes and addressing breaks.
    while the example code above is just that, an example. I dont' ever see why a framework developer would do something like this. at the very least the methodtwo() should be empty, not throw an excpetion. then there wouldn't be a breaking change.
    If the argument is "..but what if?" 1. do you want that developer on the team? 2. you're developing the framework so you should be accounting for this.
    personally i can't stand the idea of an IsImplement property. not only do i have to know about the new method, but now i also have to check to determine if i can use it? again having an empty virtual method would negate these issues.
    as i started this post, i'm still confused as to why an abstract class is "better" for changes than an interface.

  6. Avatar for Evan
    Evan February 21st, 2008

    Inheritance is also the strongest form of coupling between two implementations (classes)..you have to be able to look inside what you are inheriting from so you know what the invariants are and how the hook points should be used..although you did a good job of pointing this out in the post

  7. Avatar for Kevin
    Kevin February 21st, 2008

    use ABC as if it's an interface for version compatibility is tricky in terms of common design/programming idiom and assembly upgrade practice.
    IMHO, it's worse to check SupportMethod2 than checking if interface is implemented.
    As Evan pointed out, using inheritance to extend framework is the strongest coupling between framework vendor and consumer. In this case, use interface and interface inheritance to support versioning is the proven way, when we recall COM world's QueryInterface method.
    My $0.02.
    Kevin

  8. Avatar for Krzysztof Cwalina
    Krzysztof Cwalina February 22nd, 2008

    Jason, I tried to explain why ABC is better for evolving APIs in a talk that is accessible at http://www.researchchannel..... The whole presentation is very long. The part about interfaces starts at 2:54.40.

  9. Avatar for Wade C
    Wade C February 22nd, 2008

    The addition of SupportsMethodTwo() does nothing to solve your problem of testing for the existence of MethodTwo(). The original implementation of the ABC won't have either of these methods. It seems like reflection would be the safest answer to testing for the existence of a new method.

  10. Avatar for Haacked
    Haacked February 22nd, 2008

    @Wade, the original implemention will have it by way of the fact you added the method to the base class. Assuming you either do assembly redirection to the new version OR if the version number of the framework doesn't change.

  11. Avatar for Wade C
    Wade C February 22nd, 2008

    Yeah, I realized the loophole in my logic as soon as I posted my response.

  12. Avatar for Vladan Strigo
    Vladan Strigo February 26th, 2008

    +1 for Ayende's response.
    I would rather have my client brake at compile time than possibly on runtime. This leaves me much firmer on the ground.

  13. Avatar for h
    h March 5th, 2008

    If you guys are so afraid of breaking changes ahead why can't you figure out another way of enforcing pre- and post-conditions in your code? For example you could simply wrap every framework interface invocation inside of a runtime Reflection.Emit proxy (or inherit ContextBoundObject), which checks the invariants! That's one way and it only leads to the very first launch to be only possibly measurable slower, if even measurable at all. You can then throw exceptions just like any other framework if these invariants are not satisfied.

  14. Avatar for h
    h March 6th, 2008

    ... the rest of my comment:
    See it from the point of the developer: I'd rather get told directly when there's an exception than having to understand the intricacies of overriding public properties to say whether a method exists or not; doing so just leaves you with a mess to end with and it's that messy backwards compatibility that imho. has put the JVM/Java where it is right now. Let's call annotations "@interface", override using @Override, do c++ templates/generics (which are compile time btw.) using Erasure - so we have to CAST every object back to Object, so we don't break anything! In their docs they say: "The main advantage of this approach is that it provides total interoperability between generic code and legacy code that uses non-parameterized types" - well that's great, but never mind about the useless performance of all that auto-boxing compared to real generics, not to mention what's impossible, like instantiating generic type variables.
    Looking at how the programs execute, you're likely to have some biggish class which may be sealed or simply doesn't expose that many overridable methods, which resolves for, an invokes your interfaces - you'd have to have this control somehow... If you in this class do extensive invariant checking and inform the programmer when he has violated the invariants, then your argument about the invariants kind of fall. You could even wrap it in a proxy like stated above from here...
    Which also makes any argument that Ayende hasn't worked on a project the size of the .net framework kind of void, since he's in both NHibernate and MonoRail.
    So to re-iterate, pros of interfaces:
    - You can vary the implementation and do composition which you wouldn't be able with ABCs since there's only is-a semantics on subclasses.
    - From implementor's view: no strange and hidden state changes which is the peril of OOP.
    - Invariants CAN be checked by framework by proxying.
    - Greater degree of decoupling in the system. Less magic.
    - Interfaces inheriting interfaces solves problem with versioning to some extent, or you could version a new MS MVC 3 years from now as different, like GridView/DataGrid had done to them...
    Cons of interfaces:
    - "Too much freedom"?
    - "More breakage iff breakage occurrs"?
    - Require other less direct ways of checking invariants
    For ABCs:
    - You can add side-effects as you like. (yey.... or???)
    - You always have assembly redirection! (but hey, we're not breaking anything, just making them change code before it works!)
    - Invariant checking is very easy
    From the other blog:
    "Typically we used interfaces for all behavioral types, and classes for data objects. The reason is that behavior needs to be mocked, whereas data is just data."
    Furthermore, I figured that I could ask whether you/MS couldn't possibly make a step which isn't very common from big companies and perhaps even send your test-suite bundled with MVC, so that anyone wishing to implement these things can understand how they interact and run your tests on their own implementations? It would be really cool if you could :)
    It comes around to the question about how good your developers are; if they are good, presumably they are able to create implementations of your interfaces.
    If you wanted to add functionality without breaking interfaces, maybe even this could work to some extent:
    interface IView {} // shipping
    interface IWebForm : IView, IController, IModel {} // next version (hope not)
    in your MSMVCHttpHandlerFactory:
    someMethod() {
    var view = services<IView>();
    if (view is IWebForm)
    launchHugeOverheadWithView(view as IWebForm); // is garantuees not null
    else
    renderer.RenderView(view);
    }
    Or something :)