Further Versioning Contortions

Yesterday I had a plan to use reflection to solve the compatibility versioning problem with my software components.

Basically I need to change an interface that existing customers have implemented. And I want to maintain compatibility so that no-one has to recompile anything.

I did try to anticipate this problem by providing for changes in an abstract support class. Users were advised to extend this class rather than implement the interface directly so that future changes could be hidden from them.

Well it looks like I might be able to get away with it after all. And without any reflection. Here is a concrete example of the problem, and a proposed solution.

The solution satisfies the requirement that existing code must not be changed. Existing binaries must still work, and existing code must still compile without errors. But the interface will change. Here goes…

First, here's the current situation, demonstrated in code. I've used a simple example to show the essence of the problem.

We have ColorManager class, to which Colors can be added. Colors have names and numbers. To implement a new Color you extend the ColorSupport abstract support class which in turn implements the Color interface. Instead of implementing the interface methods directly, you implement protected *Impl methods instead. These are called by ColorSupport. This insulates you from changes to Color.

The change is that the Color.getName method is to be renamed Color.getCode, and the ColorSupport.getNumberImpl method is to be made protected (it was accidentally released as public).

Anyway, here's the initial code:

public class ColorManager {

  public void addColor( Color pColor ) {
    System.out.println( "color:"+pColor.getName()
                        +","+pColor.getNumber() );
  }

  public static final void main( String[] args ) {
    ColorManager cm = new ColorManager();

    Red red = new Red();
    cm.addColor(red);
  }
}


public interface Color {
  public int getNumber();

  // this will be changed to getCode
  public String getName();
}


// this is the abstract support class
public abstract class ColorSupport implements Color {
 
  public int getNumber() {
    return getNumberImpl();
  }
  
  public String getName() {
    return getNameImpl();
  }
  
  // this needs to be made protected
  public abstract int getNumberImpl();
  
  protected abstract String getNameImpl();
}


public class Red extends ColorSupport {

  public int getNumberImpl() {
    return 0;
  }

  protected String getNameImpl() {
    return "red";
  }
}

These classes mirror the current design of the LineListener and LineProvider callback interfaces in CSV Manager.

So the class Red represents a user created class. It cannot change. And neither can any existing Red.class bytecode.

The solution has to take into account the following: The old ColorSupport will be deprecated, but still supported. The next major version will use a changed ColorSupport and remove all compatibility code. This is allowed as compatibility can be changed on a major release. ColorSupport is a name we want to keep (for consistency across product lines). So if we use a new support class in the meantime, we have to insulate new users who implement the new, correct, Color methods. We must make sure that their code required no changes when we move to the next major version!

So here's the basic idea: apply the changes to Color, which breaks the old ColorSupport and Red Classes. Detach ColorSupport from Color and make it a standalone class. Add a method to ColorManager that can accept ColorSupport. This ensures that old Color implementations that extend ColorSupport still work with ColorManager.

Next, create a ColorSupportImpl class. This is the new ColorSupport. It will replace the old ColorSupport with the next major version. ColorSupportImpl extends the new Color interface directly. It works just the same as the old design. But we know that the name ColorSupportImpl is temporary and will be dropped. So we need to place an insulation class in between the concrete color classes and ColorSupportImpl. To do this we change the recommended way to implement colors. For every color, there is a specific color support class. For example, Green will extend GreenSupport which then extends ColorSupportImpl.

That still leaves one little problem. What about colors that we have not defined? What about user-defined colors? We need to specify that custom colors extend an insulation class rather than ColorSupport, as is currently the case. We'll use CustomColor. So this suggests a change to the standard policy across product lines. Custom concrete user classes extend an abstract custom class which extends an abstract support class that implements the interface in question.

Wow, that seems like a really complicated way to do something simple. In a normal environment you would never do this. You would refactor and modify client code. And for released software components you can't do this. Releasing commercial software creates an entirely different set of issues. In this case it is far far more important to support existing customers than it is to refactor to a clean design. The vendor has to accept the responsibility for maintaining compatibility for reasonable periods and between clear boundaries. You only need to take a look at the situation with plugins to Eclipse or Firefox to see how difficult this problem is. And they get it mostly right!

Here's the code for the new version. Watch out, we've got lots more classes!

public class ColorManager {

  public void addColor( Color pColor ) {
    System.out.println( "color:"+pColor.getCode()
    +","+pColor.getNumber() );
  }

  // this keeps the old colors working
  public void addColor( ColorSupport pColorSupport ) {
    addColor( new ColorSupportFixer(pColorSupport) );
  }

  
  public static final void main( String[] args ) {
    ColorManager cm = new ColorManager();

    // this is an old color
    // old custom colors will work this way as well
    Red red = new Red();
    cm.addColor(red);

    // this is a new standard color
    Green green = new Green();
    cm.addColor(green);

    // this is a new custom color
    Blue blue = new Blue();
    cm.addColor(blue);
  }
}


public interface Color {

  public int getNumber();

  // this is the new version
  public String getCode();
}


// this is the same as before, but no longer
// implements Color
public abstract class ColorSupport {
 
  public int getNumber() {
    return getNumberImpl();
  }
  
  public String getName() {
    return getNameImpl();
  }
  
  public abstract int getNumberImpl();
  
  protected abstract String getNameImpl();
}


// this is unchanged - just what we want!
public class Red extends ColorSupport {

  public int getNumberImpl() {
    return 0;
  }

  protected String getNameImpl() {
    return "red";
  }
}


// this is the new verion of ColorSupport
public abstract class ColorSupportImpl 
  implements Color {

  public int getNumber() {
    return getNumberImpl();
  }

  public String getCode() {
    return getCodeImpl();
  }

  protected abstract int getNumberImpl();
  
  protected abstract String getCodeImpl();

}


// an insulation class, currently does nothing
public abstract class GreenSupport 
  extends ColorSupportImpl {}


// a new standard color
public class Green extends GreenSupport {

  protected int getNumberImpl() {
    return 1;
  }

  protected String getCodeImpl() {
    return "green";
  }

}


// an insulation class for custom colors
public abstract class CustomColor 
  extends ColorSupportImpl {}


// a custom color
public class Blue extends CustomColor {

  protected int getNumberImpl() {
    return 2;
  }

  protected String getCodeImpl() {
    return "blue";
  }
}


// this hooks up the old and new interfaces
public class ColorSupportFixer 
  extends ColorSupportImpl {

  private ColorSupport iColorSupport;
  
  public ColorSupportFixer( ColorSupport pColorSupport ) {
    iColorSupport = pColorSupport;
  }
  
  protected int getNumberImpl() {
    return iColorSupport.getNumber();
  }

  // convert getCode to getName
  protected String getCodeImpl() {
    return iColorSupport.getName();
  }

}

Like I said, it's not pretty. But it does allow the API to move forward with full backwards compatibility.

The insulation classes (GreenSupport and CustomColor) are empty in the example above and will probably also be empty in the next CSV Manager release (1.2). Their purpose is to allow ColorSupportImpl to change its name in release 2.0.

And they serve another very important purpose. If in the future further changes arise that require more compatibility workarounds, they allow for the use of a reflection-based solution in ColorSupport and/or the insulation classes. Thus one layer of changes can be applied on the interface side, and one on the implementation side. This “feels” like the right solution.

Of course, some types of changes (for example, changing method access from public to protected) may not be amenable to a reflection-based solution. They may require a third layer of insulation. We'll cross that bridge when we come to it, if we cross it at all. I rely on the belief that as the API converges on an acceptable design, these types of changes will become less of a problem. Once the API has been in use for a longer period, changes become so exponentially expensive that it is better to put up with design mistakes. This is what happened with the standard Java API.

I reckon I am still able to pull this off at this stage in the life-cycle of CSV Manager. The cost will be more complex documentation until 2.0, when the compatibility code can be ditched. And the cost will be increased code complexity inside CSV Manager, which means more work for me to bug fix it all. Of course, I have a large set of unit tests so this should not be a big problem.

Well, it looks like we're set. Any final thoughts before I dive into the code?




This entry was posted in Java. Bookmark the permalink.

Leave a Reply

Your email address will not be published.