One question: do you think that an interface also has a dependency on the implementer? Say I’m a baker and I posted in my menu 2 buns for a buck. I run out of eggs so no buns. In that sense, doesn’t that error affect the contract I provided? Or would you argue that error is actually propagated to the customer?
Thanks for the question! I had a similar thought when writing the post, but the way I decided to resolve it is this: if the only way the customer can get the buns is from your bakery, they're not depending on an interface, they're depending on your implementation. But if all the customer cares about is that they can get buns *somehow* (maybe from another bakery, or from a neighbor who bought buns yesterday) then they're depending on the BunProvider interface.
This might be easier to compare to software if we imagine that the customer is using the buns for something, maybe a prepareFeast method. Is prepareFeast broken if we don't have a working BunProvider to pass to the customer? I'd argue "no"—compare the Array.map example from the post. If the caller of prepareFeast passes an invalid/broken BunProvider, that's not a bug in prepareFeast. The garbage-in, garbage-out principle applies: the results of prepareFeast can only be as good as its inputs.
To take a different real-world example: say you drive for Lyft and someone wants a ride, but your car breaks down before you can get to them. The failure in your car doesn't propagate to the rider, because Lyft will just send a different driver. The rider depends on the interface (a car that takes them where they want to go) and not your implementation.
Of course, there are all kinds of complications when we use interfaces in practice—there always are. Different implementations of the same interface are likely to make different choices about speed, reliability, the exact data/goods provided, etc. The idea *isn't* that you can swap out one implementation for another and nothing about the system changes. The idea is that you can swap out implementations and understand what the new behavior of the system will be.
Hey Ben, good to hear from you! I came to about the same conclusion as you after thinking more about the scenarios.
I think a key distinction is that dependency implies no other alternatives. And an interface allows the option of having alternative implementers. Like you said, if I need a bun, I can go to other bakers or come back tomorrow. The contact is not broken. Merely, the implementer is broken.
However, even if it’s an interface dependency and there are no alternative implementers, then it’s as good as an implementation dependency.
I think that happens quite often in practice. For example, service A maintained by one team might interact with service B maintained by another team thru an API. Yes, A might technically not “depend” on B, since they can find someone else fulfilling that contract. In practice, B’s changes and errors still affect A and I would argue that A **depends** on B. I believe these types of soft dependencies have more to do with humans than code.
With that said, thinking thru the distinction between a dependency vs. call graph is a valuable framework!
Your point about dependencies between services is a good one, and I agree it's really a human problem at that level, not a technical one. If a service you depend on is making breaking changes without warning you, that's probably not indicative of a healthy relationship. IMO an interface is a negotiation between clients and implementers.
Nice post, Ben!
One question: do you think that an interface also has a dependency on the implementer? Say I’m a baker and I posted in my menu 2 buns for a buck. I run out of eggs so no buns. In that sense, doesn’t that error affect the contract I provided? Or would you argue that error is actually propagated to the customer?
Thanks for the question! I had a similar thought when writing the post, but the way I decided to resolve it is this: if the only way the customer can get the buns is from your bakery, they're not depending on an interface, they're depending on your implementation. But if all the customer cares about is that they can get buns *somehow* (maybe from another bakery, or from a neighbor who bought buns yesterday) then they're depending on the BunProvider interface.
This might be easier to compare to software if we imagine that the customer is using the buns for something, maybe a prepareFeast method. Is prepareFeast broken if we don't have a working BunProvider to pass to the customer? I'd argue "no"—compare the Array.map example from the post. If the caller of prepareFeast passes an invalid/broken BunProvider, that's not a bug in prepareFeast. The garbage-in, garbage-out principle applies: the results of prepareFeast can only be as good as its inputs.
To take a different real-world example: say you drive for Lyft and someone wants a ride, but your car breaks down before you can get to them. The failure in your car doesn't propagate to the rider, because Lyft will just send a different driver. The rider depends on the interface (a car that takes them where they want to go) and not your implementation.
Of course, there are all kinds of complications when we use interfaces in practice—there always are. Different implementations of the same interface are likely to make different choices about speed, reliability, the exact data/goods provided, etc. The idea *isn't* that you can swap out one implementation for another and nothing about the system changes. The idea is that you can swap out implementations and understand what the new behavior of the system will be.
Hey Ben, good to hear from you! I came to about the same conclusion as you after thinking more about the scenarios.
I think a key distinction is that dependency implies no other alternatives. And an interface allows the option of having alternative implementers. Like you said, if I need a bun, I can go to other bakers or come back tomorrow. The contact is not broken. Merely, the implementer is broken.
However, even if it’s an interface dependency and there are no alternative implementers, then it’s as good as an implementation dependency.
I think that happens quite often in practice. For example, service A maintained by one team might interact with service B maintained by another team thru an API. Yes, A might technically not “depend” on B, since they can find someone else fulfilling that contract. In practice, B’s changes and errors still affect A and I would argue that A **depends** on B. I believe these types of soft dependencies have more to do with humans than code.
With that said, thinking thru the distinction between a dependency vs. call graph is a valuable framework!
Your point about dependencies between services is a good one, and I agree it's really a human problem at that level, not a technical one. If a service you depend on is making breaking changes without warning you, that's probably not indicative of a healthy relationship. IMO an interface is a negotiation between clients and implementers.