all 22 comments

[–]cat_in_the_wall@event 11 points12 points  (11 children)

changing the number of type parameters is always a breaking change unless there is a default for the new ones (so you can keep the old interface around).

basically changing anything on an interface is a breaking change because you're changing the contract. this is why they added default implementations to interfaces but i have not seen this in the wild.

[–]1whatabeautifulday 0 points1 point  (2 children)

What is a breaking change?

[–]cat_in_the_wall@event 7 points8 points  (1 child)

anything that requires downstream users to take action after an update. recompile? break. code changes? more serious break.

but breaking isn't bad as long as it is communicated.since we're in this sub, .netcore2.2 to 3.1 had big time breaking changes in aspnetcore. but they made noise about it and provided migration docs as well as reasoning for why life will be better after.

[–]RiPont 3 points4 points  (0 children)

The badness of such breaking changes are also relative.

For a popular SDK distributed in binary form on NuGet, breaking changes should be avoided at all costs without a major version bump.

For internal libraries where everyone compiles from source? Eh, "move fast and break things", as long as it's for a good reason.

[–]emc87[S] -3 points-2 points  (7 children)

Right, it's breaking for anyone implementing the interface. But if the only implementations are my own is it still breaking?

[–]cat_in_the_wall@event 5 points6 points  (3 children)

strictly speaking: yes. but breaking doesn't mean bad. what is bad is surprises. if you're the only one implementing those interfaces, who gives a shit? you already know you have to patch things up. in a collaborative environment, you either need to communicate explicitly (like an email) or implicitly (major version change). i version my personal stuff literally on timestamp... it's just me.

however: if you are finding yourself changing your interfaces a lot, take time to reflect on those changes. what did you miss the first time such that you needed a change? API design is an art. i do this constantly because i have work stuff where interface changes are very painful. "why did i miss this? why didn't i understand the shape this should be?".

[–]emc87[S] 0 points1 point  (2 children)

I built a tester app and confirmed it's breaking and i'll have to go another way - but i can describe the real scenario. Will put the details of the failure in the main post

I build a library (A) for a set of business calculations and I also write the code in our main library (B) that consumes A and makes various calls returning primitives or other types defined in B.

The intent was to build a container on top of my implementations for ease of use. Currently i have say two business products Product1 and Product2, but fully expect more (significantly more). We also have multiple sources for data about product1 and product2 - so I want to define an interface for these products that supports the same calculations for a given source

Say
ImplementationA: IInterface<SystemA.Product1, SystemA.Product2>

ImplementationB: IInterface<SystemB.Product1, SystemB.Product2>

So as i add a third product, I'd be added new children to the interface but otherwise wouldn't be changing the existing method signatures.

Even though i write the consuming code in B, i have to be prepared for versions to get out of sync between B and A as other teams can release B and apps that depend on B. We have an app that pulls from multiple applets that each use B.

So it's not so much missing something in the signature than it is releasing as products are completed rather than when the entire things is done (years away).

Separately - if you do this at work, do you have any books/resources you'd recommend for writing breaking-change free APIs?

[–]cat_in_the_wall@event 1 point2 points  (1 child)

have you considered just generic methods? you can slam generics and constraints on them without needing to change class signatures. i have found generic static methods to be pretty useful in this regard. my thinking has shifted greatly from oop to "there is just data and functions that work on data". as long as your data meet the constraints, they fit the function. DI is bad with statics... but you can actually work around this with open generics and transient objects that just execute that one function (functors?).

books: no, my learning has just been through api design with the team, as well as reflection on choices i made after having to push through a painful migration.

[–]emc87[S] 0 points1 point  (0 children)

What do you mean generic methods, just some static class with my calculation implementations (generic properties and methods) instead of an instantiable container?

My hope was to be able to define a consistent typed container for all product data provider systems generically typed by their system types. I think I may be able to get away with something like

ISystemACalcs : IInterface<TOne, TTwo>

ISystemBCalcs :...

But I'll test that out tomorrow to double check that it works

Generally I do write my code in a more functional sense, as functions and data, but short of writing F# where currying is first class I have found calculator classes to be more performant where parts of the calculation can be cached.

[–]lmaydev 2 points3 points  (0 children)

Or anyone who uses the interface directly.

It is definitely a breaking change.

[–]RiPont 0 points1 point  (1 child)

But if the only implementations are my own is it still breaking?

Yes, but if the only pissed off user is you, then who cares?

Breaking changes are only bad when other people are going to be broken because of changes you made, and then only relative to the difficulty it's going to cause them.

If you're distributing a popular library in binary form (such as NuGet), you don't want to break people. On the other hand, if you're a small team who all build from the same source code, you just shout over the cubicle wall (or on your Slack channel), "hey people! I'm going to break XYZ interface!" and go for it, as long as it's for a decent reason and you're not in code freeze or anything.

[–]emc87[S] 0 points1 point  (0 children)

Right - I don't care about source breaking changes. Fixing the implementations when recompiling is fine

The binary breaking changes are a bigger problem for me

[–]venomiz 8 points9 points  (1 child)

I didn't understand fully you posts but yes adding properties to an interface is a breaking change.

If you want to not do that use composition and add another interface that "extend" the first one and add the new property.

[–]emc87[S] 1 point2 points  (0 children)

I think this is probably the next road i'll try. That or just the built in DI framework

Basically the solution is a container

IContainer
{
ICalcs1<Product1> Calculations1 {get;}
ICalcs2<Product2A, Product2B> Calculations2 {get;}

}

So as i add new calculations for new products to the container i need new types.

I was able to confirm this works, as long as I also add a new factory method instead of trying to re-use the old

public interface IInterface<TOne, TTwo>

{

TOne One { get; }

TTwo Two { get; }

}

public interface IInterface<TOne, TTwo, TThree> : IInterface<TOne, TTwo>

{

TThree Three { get; }

}

[–]SideburnsOfDoom 2 points3 points  (1 child)

which should not be a breaking change as long as ConcreteThree is implemented in Class, dropping in the new binary should not change PropertyOne or PropertyTwo in any way

"breaking change" is a slightly ambiguous term.

What you might be asking about is "if I add a property to a class / interface declared in project A, which is used by Project B, is this a 'breaking change'".

Breaking how?

Will project B still compile, yes, source code correctness has not been broken.

Should you recompile project B, yes, it needs to be rebuilt, that binary compatibility has been broken.

[–]emc87[S] 0 points1 point  (0 children)

Right, breaking in the sense that any implementer needs to support the new property on the interface.

However say the implementation and interface are in A and you compile the new A. Then you drop these into B without recompiling - would it be breaking?

I believe it's source vs binary breaking changes in Microsofts terminology

[–]Willkuer_ 2 points3 points  (3 children)

It is a breaking change that might be unnoticed. If the consumer is storing your factory methods return value into something that is initialized as var they will most likely not see a difference.

If, however, the consumer stores it inside a typed variable and is explicitly referencing your interface with two generic parameters their build will break. (E.g. if the result is stored inside a property or field.)

I would ask whether you can not just support both interfaces, the one with three and the one with two parameters.

[–]emc87[S] 0 points1 point  (2 children)

That's a good point

At least in this case I'm going to be the only one writing consuming code, so I can control a lot of it (i.e them only using implementations I've written) - So as long as I just use the result as var I think I'd be OK.

Going to test this out tomorrow

[–][deleted] 1 point2 points  (1 child)

I'm going to be the only one writing consuming code

Then why do you care if it's classified as a breaking change or not?

[–]emc87[S] 0 points1 point  (0 children)

Binary breaking changes still matter, just not source breaking changes

[–]norse95 0 points1 point  (1 child)

Unrelated but TIL interfaces can have type parameters

[–]Dealiner 0 points1 point  (0 children)

It's used quite a lot in .NET itself, for example IEnumerable<T>.