0

Say you have an interface with a function List<A> doSomething(Collection<B> bs). Looking at Java's List interface and its JavaDoc I could return an immutable List which doesn't support addition, it could also be mutable but not allow null values (which some implementations actually don't allow). Random access could be fast or slow (LinkedList).

I want to change the List implementation that is returned by doSomething which behaves differently from the currently returned List implementation but strictly speaking it's a breaking change, isn't it? I am really struggling with this as I don't want to release a major version just for this little change.

But there is also Hyrum's Law: [...] all observable behaviors of your system will be depended on by somebody. [...]

Where to draw the line here?

13
  • Is there anything I should clarify? Would like to improve the question given the downvote
    – roookeee
    Commented Jul 29, 2019 at 19:18
  • If your change still conforms to List<T>, then it's not a breaking change. Commented Jul 29, 2019 at 19:30
  • It conforms to List<T> but you won't be able to add to the List anymore, thats why I am asking.
    – roookeee
    Commented Jul 29, 2019 at 19:30
  • 1
    Then it doesn't conform to List<T> anymore, does it? Commented Jul 29, 2019 at 19:31
  • There is Collection.emptyList, Arrays.asList and Java11 List.of which each return an immutable List which is explicility allowed in the JavaDoc of the List interface. Thats the whole struggle
    – roookeee
    Commented Jul 29, 2019 at 19:31

1 Answer 1

3

I think the key insight here is that whether or not a change is "breaking" has nothing to do with whether or not the change seems, to you, to be a minor or major change, and has nothing to do with how broad the scope or complexity of change is.

Instead, the only thing that determines if a change is breaking is: does code that use to work before the change no longer work after the change?

It is up to you how strict to be. For instance, is it OK to break code that explicitly should not have worked, like originally a list was not listed as allowing nulls but due to a bug you weren't properly checking for them? That's up to you in your change management policy - some consider bugs OK if they break code, some say that if any conceivable code existed that would have worked before (even if you have no proof that such code actually exists) then the change should be considered breaking. Again, that's up to your policy to decide how to handle.

If you have a policy that says that major versions can be breaking but minor versions cannot, then yes - any change that is defined as a breaking change means you cannot include it in a minor version. If that is the only thing that changes, then yes that means you would have to do a major version change to include it.

This problem is created because you have combined two orthogonal things: whether or not a change is minor or major, and whether or not it is breaking. Because one depends upon the other, it means that something is minor in scope yet breaking, it is promoted to a major change. This is not a bug, though, it is an intentional feature of this style of semantic versioning.

However, there is a way to avoid some of this in the future, which is this: if a class is generic or fuzzy in what it allows and supports (and List is about as fuzzy an example as you can find), if you want to be more picky than that you are often best served by creating a wrapper or inheriting class, i.e. a PickyList with all sorts of rules about what it accepts and what it doesn't. You can even include a warning that add() rules can change over time due to changing external rules (common in regulatory settings or when external constraints are not defined in the same code - imagine having a class that adds to a financial market order book, for example), and so all consumers of this function should be sure to not assume add will succeed and handle errors/failures appropriately.

In the end, remember the wisdom behind why breaking changes should be handled with care and additional burden: code rot and hard to update libraries are a massive problem in IT today. The level of difficulty significant projects have in just trying to keep versions updated every few years is disgusting and, quite frankly, insane. "Managing software version hell" has joined "ability to Google" in my list of skills that every software developer must have and yet never show up on a job description.

Anything you do to fight the good fight against code rot and version hell from the infinite march of "breaking change with questionable benefits or necessity" is a sacrifice towards the greater good, and I thank you for your service in this worthy cause.

3
  • Thank you for your lengthy and well worded response :) I concur to what you are saying and am split about what to do. Will add this to a list of changes I would like to do for 2.x instead of breaking now, it's just not enough to justify a major release. Robert Harveys point of view was food for thought too though...
    – roookeee
    Commented Jul 29, 2019 at 20:03
  • Good practical suggestions too, won't do that error again
    – roookeee
    Commented Jul 29, 2019 at 20:10
  • 1
    There is another consideration if a change is a breaking change: If any code stopped working, was there a reasonable expectation that the code should have worked, or was that code exploiting a bug. If the changed behavior could not be reasonably inferred from the documentation, including the language/framework documentation, but only found by experimentation, then I would not call it a "breaking change". Commented Jul 30, 2019 at 7:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.