63 comments

[ 4.6 ms ] story [ 128 ms ] thread
I'm torn on this. I agree in principle with the article, but what I've seen in practice having worked on different projects (mostly Java) is a lot of one interface to one implementation class. The interface and the implementation ends up being nearly the same.

Along comes a need for a second implementation years later. Now you have to figure out how to match the interface but also do some serious refactoring to truly make the interface more generalizable.

It is easy to say that doing it up front to genericize functionality in an interface but in practice I've seen that it's difficult to do right. When I create a new interface I hope that I did my best job with abstraction and that I give the least amount of rework to future developers.

I think the purpose of (Java) interfaces should be primarily seen as a means to decouple code that shouldn't be tightly coupled, rather than a means to offer polymorphism. It's relatively rare that multiple implementations of the same interface is desirable, it definitely happens, but not too much.

It's a bit of a balance. Decoupling can definitely be taken too far. Anyone who has worked in the language has no doubt encountered the ravioli code left by someone who has just read Clean Code and has fully embraced Uncle Bob as their lord and savior.

In the end the need for this interface-as-decoupling pattern has dropped significantly with modern dependency injection and ad-hoc mocking libraries like Mockito. They grant most the benefits of these single-child interfaces with virtually none of the drawbacks.

Generalization is a way to go forward. Years back, they didn't even dreamed about your new class. Refactoring isn't waste. Its what enables good code this time around. (And what avoided mistaken decisions back then as someone just assumed stuff will be refactored)

But those whole years compilation speed benefited greatly (because in Java interface is a cutoff for incremental compilation - in Java interface is THE developer experience).

This is your brain on java.
Good reflex, but nope! I don't write Java anymore and possibly never will again. I think the point stands on it's own, regardless of language.

This is basically why I don't like Java -- it's so uncomfortable that it puts people off of types for the rest of their lives.

Languages with good, ergonomic type systems are out there, that don't have the needless ceremony and expended effort of Java and people pass over them at their own peril.

The wider trend is also coming back to statically checked types -- Python's typing module, Ruby's Sorbet, the ever growing popularity of Typescript, etc.

In general I find that people restrict the idea of "interface" too much. If it has a downstream user / client, it is an interface despite not being `interface X`. This becomes very obvious in a language like Rust where you are encouraged to use concrete types as your 'interface' to downstream clients / consumers / library users.
This is your brain on haven't read the article.
I know that there's automated title mangling going on, but changing 'unquestionably' to 'always' seems outside the automatic title mangling, and fairly useless semantically.
Sorry I'd hit the character limit!
Ah, then I'm sorry for my rudeness! I thought it had been done by some automated process, which would have been inappropriate; but it seems perfectly appropriate as the work of a thinking human editor working around constraints.
Might have once agreed in my young OOP days but after 20+ years of experience, totally disagree. Yagni. Don't add unnecessary code and abstractions. Minimize complexity wherever possible.
Welp definitely disagree -- pardon the assumption but when you say "young OOP days" I instantly think Java/C# and I think the baggage in those languages leaves a bad taste when it comes to abstractions that were supposed to stay light.

Of course it would have been impossible to jump to the future for you and use better languages, but modern languages with static type systems (Typescript, Rust, etc) reduce the difficulty of writing and maintaining interfaces so much that even if you never need it you benefit.

I also still think that the documentation benefit (seeing structure without implementation) is worth it.

> Minimize complexity wherever possible.

Fully agreed here, pretty much unassailable.

Just because a language makes it easy to create and maintain interfaces doesn't mean that interface was a good idea to begin with.
in case isn't clear to other, types and interfaces are effectively synonymous in Typescript. typescript doesn't really bring in that other cruft unless you want it
types are so synonymous that there is no reason to use interfaces in TypeScript.
I would be impressed to see readable, non-trivial TypeScript code that does not use Interface.

Unless you are referring to interface as a concept, and not the keyword in TS.

Can you use the “class MyClass implements MyType” keyword with types?? That might be the only reason if you can’t
There are several reasons to use interfaces in TypeScript:

- they perform better at build-time and in editor

- they have more relaxed rules for recursion

- they can help identify complex types and distinguish them from simpler ones

- they’re guaranteed to be usable to enforce class implementations, whereas only simple types can be used

- they’re much easier to read in type errors

> - they have more relaxed rules for recursion Do you have a source for this? I would like to read more.
You can certainly write java/c# without abusing interfaces. They're just available, not forced on you.
Agreed. This mindset has turned parts of the rust ecosystem into a mess. It's frustrating coming across a generic function argument, for example. Like, what goes there? Instead of a primitive type, or a struct/enum with its own documentation in a provided link, you find a dead end.

Interfaces are nice when used appropriately, but the added complexity by default isn't worth it.

I think it’s pretty much necessary in Rust to allow things to interop with low overhead, which is an explicit goal of most Rust code. That said, this article is not talking about interfaces as in Rust traits or Go interfaces necessarily but more the general concept. You can see that more evidently by the way that they point out that they don’t necessarily mean to imply that people should be writing entire plugin systems. What’s going on with Rust traits is much, much more granular than what they seem to be discussing.
Having everything behind an interface is pretty much essential for strict TDD in pure Java 8 (I don't know if or how things have changed for later releases). Of course there are a plethora of mock frameworks that will hide the details, but I'm fairly certain under the hood it's dynamically creating an interface. My inclination is to prefer making that explicit rather than relying on magic.
What goes there? Anything that meets the specified requirements of that type
Definitely seen a lot of developers who, every time they want to go use a library- metrics, logging, http-clients, they go build their own TypeScript wrapper and interfaces. They think they're doing us all a huge favor by giving us something that might be portable, and which has an interface which is this way or that way easier or better. But it becomes a sticky not quite-sensible mess pretty quick, and ultimately, and no one can StackOverflow or nom search their way to success, since the internet has nothing to say about our homegrown overgrown wrapper + helpers.

I really want to find ways to better capture "we use X for logging along Y and Z plugins and U and V configurations". Making top down tools, that hide whats afoot, is just plain bad, but copy pasting the same solution dozens of times also doesnt fly.

Anyhow that all said, my problem here is more about encapsulation masking whats really happening, which I think is treacherous. I dont necessarily agree with the author; etcd for example I think has huge epic wins the author just doesnt get & that databases have simply never caught up on (common Http-centric interfaces, watch-api). But I did do many years of java and the Service Provider Interfaces (SPIs) everywhere were a cherishable & adorable source of great power & flexibility. That point is retained. I just think favoring sql is a terrible terrible example that ignores how excellent & typical http based interfaces are.

Sometimes it makes sense to write a wrapper. I've worked on a team where every few months we'd have some mandated framework change, or a microservice we depended on was being replaced by some new project. In that case it's easier to update an abstraction in some common library than ripping the code out from every place it's used in application code.
I love the rule of three. I know I've heard it on the Go Time podcast, but I'd heard it before then.

Copy your code once. If you find you've copied it again later, that's the right time to think about whether you'll continue to, and therefore whether it's worth making something reusable.

So yeah, wrappers are nice. But I'm also a really big fan of lazy wrappers. Wrap the thing barely enough to get the job done so you can upgrade in one place, but don't get fancy because you will probably hate yourself for it down the road.

I hate that rule. It makes almost entirely sure that you'll loose control for your duplicates. The way around is way easier
It can be worthwhile replacing a wide interface with a narrow one. If an external interface has 30 methods but you only use 3, declare an interface with those 3 methods. Go makes it easy.

This is the interface equivalent of removing unused function parameters. Most people would say you shouldn’t add extra, unused parameters to a function just in case you might need them someday. Why require methods that you don’t use?

The author hates on mysql with no reasons for their hate. Quite odd even if their examples of projects that would have benefitted more from more interfaces are good.
Hey, author here -- it is a bit odd and also possibly immature to be so harsh on MySQL apropos of nothing. That said, I intended more to be pro-Postgres rather than anti-MySQL (it's a great piece of software, other DBs and Postgres learn from it all the time, zheap[0] exists to replicate what they've built, for example).

I also have to admit that I definitely want postgres every time I see MySQL. Maybe I need to read more on just how easy and amazing MySQL can be. Links welcome!

[0]: https://cybertec-postgresql.github.io/zheap/

Would love to hear some concrete reasons here. What about postgres makes you avoid MySQL?
There are a ton -- in general flexibility, wealth of enterprise-grade resources and support, community, speed of improvements and rock solid operation.

For just one real concrete reason, postgres's extension ecosystem is second to none, AFAIK.

Hey author here -- just to make it clear, this isn't a rah-rah Java post. At this point I actively avoid Java and spend my time in languages with advanced, ergonomic type systems (Typescript, Rust, Haskell).

I know what it sounds like, but I think the point stands -- for large components if you don't write an interface, you (or someone else who wanted to interoperate/shim/hack) will regret it.

By interface do you mean an OOP style interface as in the C# (and I think Java, been many years since I touched Java) keyword? Or do you mean some sort of intermediary 'wrapper' code rather than calling the API directly?

The latter I think can have value even if you only have one implementation as it makes it easier to find and update all the places that touch the underlying API in case you ever do need to change. It can also give a clear picture of the surface area of the API you're actually using all in one place in the codebase.

The former I think is less valuable and often a bad idea. As others have pointed out, unless you actively maintain and test multiple implementations there is little chance you will actually be able to effectively abstract across them and just drop in a new API / 'backend'. You will end up having to rewrite and refactor code that uses your interface anyway to actually swap to or offer an alternative. At that point a C# or Java style interface isn't buying you much and is just adding complexity and code duplication (and potentially impacting performance).

The calculus may look slightly different for internal projects that your team fully controls (where there is less value in an interface) vs. open source projects that want to make modification easier for others (where there might be more value).

The examples you give of projects like Ghost dropping support for alternative databases I think reflect the reality though that if you're not actively maintaining and testing multiple implementations (which is extra work that may not be justified for the value it is bringing) then interfaces are just adding overhead now for a future feature that you may never need to support and will have to do some rewriting even if you do. YAGNI.

Some people on this thread are talking about the java interface keyword in general whereas it seems to me OP is talking specifically about interfaces to backing services [0].

[0] https://12factor.net/backing-services

I read that and it seems conceptually similar to the intent of Java interfaces FWIW.
I mean using Java interfaces for backing services, as opposed to using Java interfaces for every Java classes.
Well that I actually just take that idea as gospel/the basics at this point. In 2022 most applications should be doing things like relying on REDIS_HOST or REDIS_URL as ENV/config and using that to connect. 12factor enshrined these ideas and it's the way to go, I think.

That said, the services that I mentioned where I wanted this pattern actually already do treat their backing services this way, so the idea is more about the general idea of an code-level interface.

Yes, you go further than 12 factors with my favorite point of the article:

> It’s much better to have a DataBackend that has implementations like Postgres14DataBackend and MySQL8DataBackend rather than just changing the driver: 'postgres' to driver: 'mysql' in some configuration file.

An interface isn't a magic get out of jail free card that makes swapping out an implementation trivial. And if it is, it's because it was done so from painstaking effort, and probably maintaining at minimum two implementations.
I agree, but not having an interface is a great way to make sure you probably are never able to swap out the implementation (even if someone came along and wanted to do all the work).

Drastic fixes and Rewrites almost always consist of three things:

1. Writing tests to ensure functionality

2. Writing an interface or shimming the existing functionality to redirect control flow

3. Implementing and testing the rewritten functionality/improvements

Obviously you can't do it everywhere, but life is much better when (2) was done up front.

It depends on a dozen details, but one of the strengths of a language like Java for me atleast is that it's pretty easy it is to do those things fairly bug-free.

One of the biggest dimensions that completely change this conversation is: module boundaries. If you're in a position to touch all of your user's code, it's night and day vs if you're pushing migrations on other teams or God forbid customers.

But if you have all of your code? In Java atleast, it's cake.

Really you should have those tests upfront. After that noone should be scared of changing any part of the codebase. Let alone 'never' changing stuff.

Releasing rewrites / redesigns behind a feature flag, with a couple of strategic if statements is usually good enough. Noone cares that you had to change code outside of your interface for a release or two.

Interfaces do not solve Expression Problem. So not even having interface can guarantee success.

(Finding abstractions to solve given expression problem ain't easy too, but ultimately it may be better effort for library authors)

> probably maintaining at minimum two implementations.

Manual TDD (not using a mock framework) practically requires dependency injection and thus interfaces[1] and a minimum of two implementations for each: the real one that does the work, and the one or more with minimal functionality that are passed in to other components when they're doing their own unit tests. Sometimes it even makes sense to have more than one test implementation depending on the needs of the various components that need to test against that interface. And in my experience that does act as a forcing function for making swapping out an implementation trivial. Or, as I prefer to call it, properly separating concerns.

[1] Or I suppose one could use an abstract class, but I've found interfaces cleaner.

wrong. if your implementation is simple enough you dont need the interface.
Sure, the implementation is simple on day 0/1/7/15/365. Depending on the project, eventually it may not be, and you'll regret not better defining the functionality.

Again, it's not that every single piece of code in your repo needs an interface, just large components, like data stores.

Disagree.

It might be a useful pattern if the language you're coding in has some other issues that arise when there's no abstract interface (can't think of an example but I have a vague memory there are some).

The example of kubernetrs should have an storage interface, instead of directly on etcd is great.

I once suggested to GKE's relevant team that they should refactor K8s to have a genetic storage API that wraps etcd. They instead decided to build a etcd mock.

I guess at this point, etcd is pretty much a subproject of k8s, instead of general purpose kv system. So that would be OK.

k8s is actually a story of the triumph of interfaces. CoreOS may have stumbled on to it first, but k8s's real power is in the Operator (CRD + Controller) paradigm. Operators are essentially interfaces-as-YAML + implementations-as-connected-services.

So much of the successful simplification of k8s today is due to being able to move complexity out of tree and delegate functionality to external pieces. The sort of interface-interface that CRDs represent is doing a lot of heavy lifting in being able to simplify k8s.

K3s ultimately added pluggable (somewhat) k/v layer
Obviously you don't _always_ build an interface. That's how you end up with HelloWorldWidgetFactoryImplementationDataObject.

The real question _should_ be: "At what point do you make an interface between function calls, given varying probabilities that there will be one or more implementations"

Using interfaces everywhere might increase future flexibility, but it comes across as (a) not accepting the idiomatic way of programming in Java, C++, Rust, or C#; and (b) a form of premature optimization (which according to Knuth is the root of all evil, as we all know) -- a lot like premature abstraction.

Duck typing is great, but interfaces everywhere isn't necessarily the answer in non-duck OO languages.

You could falsely negate a bunch of things by claiming it's premature optimization if you expand it's definition to mean thinking in advance.

That being said I agree with you that interfaces for literally everything is a bad idea. Especially depending on interfaces everywhere, given that it's often quite nice to know concretely what some piece of code is trying to do.

The original post isnt super clear to me regarding how often the author actually recommends using interfaces. The examples used are for storage backends in projects like Kubernetes, which most people would probably agree is a pretty good context in which to utilize interfaces.

>"... is unquestionably right"

To the author's credit the post is at least categorized as both fluff and a hot-take. :)

Honestly, within a single moderately sized codebase for your average boring CRUD app, i'm not sure why you should have interfaces for everything.

99% of those will never be as popular as Kubernetes, Graylog or Ghost. And it's not like introducing those interfaces wouldn't be connected to lots and lots of work: like in the case of Ghost, you cannot be a part of the crowd that suggests that ORMs are bad most of the time and then also suggest that interfaces are good and you also should abstract away the database in some other (possibly infeasible) way.

It's not like you cannot do that, but you absolutely cannot do that in a reasonable amount of time. When your boss is asking you to ship features by the next tuesday, you don't even have the liberty of thinking about what is generalizable and what ever will be - you just write the implementation directly and get it done.

And if you ever actually need an interface? Just extract one with your IDE. Is it all too polluted with implementation details to do that, because of the aforementioned time/resource limitations? Then simply rewrite it anew, throwing all of the gunk out. You cannot rewrite it because of its size and coupling to the rest of the system, you say? Then you don't actually need the interface - don't poke the codebase with too much refactoring, lest it fail.

The point i'm making is that while most projects out there will never have the need or resources for having interfaces for everything, even the larger projects that could have them (like the example about Kubernetes), might be so complicated that introducing them might result in a lot of breakage despite your best efforts and would reflect badly upon the entire project.