Hacker News new | ask | show | jobs
by gentleteblor 3391 days ago
I'm not sure how I feel about this.

What has worked for me is embracing change.

Requirements will change, third party integrations will change, front end frameworks, use cases, service providers etc.

Everything will change so write (really build) code that tolerates change (IOC, interfaces, etc. Any form of loose coupling you can get away with cleanly).

There are of course situations when the one line fix is the right one. But in general, this outlook on short term vs long term changes (knowing when the concrete should become the abstract) is one of the differences between being a programmer and a software engineer (or being a senior and a junior).

2 comments

> Everything will change so write (really build) code that tolerates change (IOC, interfaces, etc.

Everything might change. Not everything will change.

I think the problem I have with your principle is that many of the techniques to accommodate change (the "IOC, interfaces, etc" you mention) can have a real and immediate cost when it comes to understanding a codebase, and that's the fundamental sin when it comes to maintainability.

Abstraction and indirection have value, obviously, but on balance I'm with GP; don't add 'em until you actually need 'em. In particular, don't add an interface and a factory and whatnot until you actually have more than one implementation.

This may depend on your languages and dev environment; something like IDEA where large-scale but mechanically trivial refactorings can be accomplished quickly and safely are more suited to KISS than e.g. dynamic languages where tooling is much much weaker.

> Everything might change. Not everything will change.

True. Not everything will change. But you don't know which ones will and which ones won't. So be ready.

> I think the problem I have with your principle is that many of the techniques to accommodate change (the "IOC, interfaces, etc" you mention) can have a real and immediate cost when it comes to understanding a codebase, and that's the fundamental sin when it comes to maintainability.

Not true. The benefits of IOC and loose coupling are well established and I won't even bother relisting them here. I'll only say that those benefits usually outweigh the minor indirection you get as a result.

The question then becomes, do you loose couple from the beginning? or tightly couple everywhere and loosen as you go. I tend towards doing it from the start, for consistency sake (help those poor junior engineers out), and for all the other benefits (which again, i don't want to list, but i'll add the one biggie - unit and integration testing).

I'll also note that IOC/loose coupling is rarely the cause of the over abstraction you fear. I've seen it way more in domain models or APIs.

There are many fads, new fangled doo-dads, thingamajigs in our software world. New frameworks, seismic shifts in architecture, herd mentality etc. IOC/loose coupling isn't one of them. It's good, old engineering practice.

You underline what is essentially my biggest gripe with IOC. There seemed to have been an explosion of everyone wanting you to use a new fancy framework to make it happen.

For the most part, constructor logic that simply sets dependent fields (and, importantly, does not do anything) along with not requiring fancy initialization shakeups goes a long way to easily testable things.

That said, people that go out of their way to make a fixture for each individual method can be just as annoying as the folks that only do end to end testing.

Basically, life is easy to argue at the strawman level. Breaks down quickly when there are "boots on the ground." (Or whatever other saying works.)

I'm with you there man (or lady)...the pace of "new and shiny" is exhausting.

One of the things I like about our environment is that we're always disrupting ourselves (maybe a little too much). So while it's true that there are many heavy weight DI tools (Castle.Windsor for ex), there are also lighter-weight ones (Ninject and co). At the end of the day, we each pick our heavy we want our tools to be (we can always implement IOC without a DI tool if you want to keep things bare boned).

> Basically, life is easy to argue at the strawman level. Breaks down quickly when there are "boots on the ground." (Or whatever other saying works.)

It's an interesting discussion for sure. And some of it is philosophical. When the "rubber hits the road" (or whatever saying works) and i'm stuck in the office at 2am trying refactor a ball of mud, I wish those who came before me had thought of this stuff.

Engineering. Not Programming.

I find it extra funny when "new and shiny" is actually a 30 years old idea in a new spin. (Reactive programming haha.)
Adding a proper interface is much more expensive than removing it if it proves worthless. Designing a good interface and especially components or other means of compartmentalization removes a huge source of mistakes.

Especially the last part is critical - if there is no abstraction or some other kind of tight binding between components you've already lost.

I think we may be miscommunicating; a lot of these words mean different things to different people. Designing a good and minimal "interface" for a class or module, in the sense of how it exposes its functionality to the rest of the world, is absolutely important, yes.

Deciding that mentioning classes anywhere in a signature is Evil, and that everything must implement a separately-defined interface in the Java/C# sense, is IMHO not at all useful. Not because it's "expensive" - it just takes an already-designed class API and copypastes it into an interface - but it's pure busywork. More code (which some people treat as a plus), harder to understand (because now there's always extra ambiguity at a call site as to which implementation is being called), adds nothing. This type of thinking - taking a useful tool or rule of thumb and turning it into dogma - seems far more prevalent in Java-land than in any other language I've worked in; I've never really understood why.

My motto is "Design for deletion."

Your first priority is to design your code for the inevitable day when your successor (or perhaps you yourself) dislikes it. The less effort/risk to uncouple and remove it, the better.

This helps avoid the well-meaning pitfall where you design things to be "customizable in the future", but the ways it needs to be customized aren't well known, and it turns out you've only complexity that gets in the way of the changes you eventually want.

In contrast, any code that's easy to remove implies that there's a good boundary, interface, or contract.