Hacker News new | ask | show | jobs
by monocasa 2013 days ago
Hard same.

The best organizational level technique I've found so far is to add the rule of three to code review checklists. An abstraction requires at least three users. Not three callsites, but three distinct clients with different requirements of the abstraction.

Obviously it's not a hard rule, and we allow someone to give a reason why they think that it's still a good idea, but forcing a conversation starting with "why is this even necessary" I feel has been a great addition.

5 comments

I’m curious why three call sites isn’t sufficient. Any time I find I have three instances of the same non-trivial logic, I immediately think of whether there’s a sensible function boundary around it, and whether I can name it. If I can, it’s a good candidate.

Obviously for trivial logic that’s less appealing. And obviously all the usual abstraction caveats (too many options or parameters are a bad sign, etc) apply.

The risk with so much duplication is that if the logic is expected to remain the same, even tests won’t catch where they diverge. To me that’s just as risky if not more with internal call sites than with clients, as at least client drift will be apparent to other users.

Abstraction here likely means more than a function - maybe something like an interface base class?
Probably. When talking about object oriented programs, "abstraction" is oftentimes used as a placeholder for "abstract class" as opposed to a "concrete class". You can see this at play when talking about the SOLID principles and when you get to the "D" part people want to turn every class into an interface because it says you must "depend upon abstractions, not concretions".
I think this is where I’ve been most at odds with common OOP approaches (apart from the common practice of widespread mutability). An interface should be an abstraction defining what a given operation (function, module) needs from input to operate on it and produce output, and nothing more. Mirroring concrete types with an interface isn’t abstraction, it’s just putting an IPrefix on concrete types to check a design pattern box.
A function is an abstraction as well. In case of a function, a 'client' of the function is the call.
That’s not what I took from it, but even if that’s what was meant, I think I’d have the same reaction. In terms of abstraction implementations, a class is just a different expression of the same idea of encapsulation.
Given the context of the posts that it was replying to, my impression was that they meant the "rule of three" applied to an entire abstraction layer.
I still don’t think I’d react differently. A function is an abstraction layer. Maybe this is just me being unintentionally obtuse because I’ve worked so long in environments where functions or collections/modules of functions are the primary organizing principle, but when I encounter “premature abstraction” arguments I don’t generally understand them to mean “sure take those three repetitions of the same logic and write a function, but think really hard about writing a module/namespace/package/class/etc”. Am I misunderstanding this?
I agree with the sentiment. A pure function with one or two parameters is going to attract a lot less scrutiny than a whole module with multiple classes.
I recently ran into the very same thing.

Instead of creating a Spring service to call a repository for data retrieval, i instead called the repository directly, because there was just a single method that needed to be implemented for read-only access of some data.

And yet, a colleague said that there should "always" be a service, for consistency with the existing codebase (~1.5M SLoC project). Seeing as the project is about 5 years old, i didn't entirely agree. Even linked the rule of threes, but the coworker remained adamant that consistency trumps everything.

I'm not sure, maybe they have a good point? However, jumping through extra hoops just because the software is a large enterprise mess doesn't seem that comfortable either, just because someone decided to do things a particular way 5 years ago. It feels like it'd be easier to just switch projects than try to "solve" "issues" like that (both in quotes, given that there is no absolute truth).

I think its a judgement call to be made. Being consistent with a deliberate architectural decision that is actually useful is important. Otherwise you could potentially have a broken window effect where more and more calls leak out of the service layer with the justification being if it was OK in one place why not others? Putting it in the service means that it is ready for any new calls that might be added and future collaborators know there's generally only one place to look for these calls. Now maybe in this situation it would be overkill but with bigger and longer lived the project, the more consistency pays dividends.
Well, consistency in itself is a good rule to follow. The problem is , if a bad decision was made at the beginning of the project, maintaining consistency despite that is madness.
Hey, it wouldn't become a 1.5M sloc codebase if these rules weren't followed! ;)
Yep consistency truly matters, since its likely this won't be the only need for data retrieval and everyone doing their own special thing means the code becomes an unreadable, in-consistent mess that cannot fit in anyones heads and development velocity slows to a crawl.
From where I am, there are abstractions coded for APIs in the layers of - topmost API layer, then Business logic and the 3rd DAO layer. Even though there is only one implementation everytime of these layers, this structuring alone has helped maintaining the code so much easier, as everyone even across teams goes by this structure while defining any API. Can't even imagine just coding functions in large codebases without a pre-defined structure, it can become brittle over time.
Large functional codebases have their degree of organization too, be it modules, namespaces or something similar.
Does this also apply to UI? I think a lot of front end libraries entice developers to fall for those early abstractions.
Why 3?

Rule of three sounds catchy but logically it's just a arbitrary number.

Similar to SOLID and KISS, why pick some arbitrary (and also obvious) qualitative features and put it into an acronym and declare it to be core design principles?

Did the core design principles just Happen to spell out Solid and Kiss? Did it happen to be Three?

Either way, in my opinion, designing an abstraction for 3 clients is actually quite complex.

The reason the OP advocates pure functions is because pure functions are abstractions designed for N clients, when things are done for N clients using pure functions the code becomes much more simpler and modular then when you do it for three specific clients.

This is a good question, and I haven’t yet seen anyone reply with (I think) the real answer: it’s not the rule of 3 so much as “not 2”.

When you start adding a new feature, and notice it’s very similar to some existing code, the temptation is to reuse and generalize that existing code then and there -- to abstract from two use cases.

The rule of 3 just says, no, hold off from generalizing immediately from just two examples. Wait until you hit one more, then generalize.

“Once is happenstance, twice is coincidence; three times is enemy action” (Ian Fleming IIRC)

I think setting hard limits on design is a good thing. Creativity needs limits. If your limits can imply something about your desired design goals then that’s a good synergy. It also forces the engineers to think more about design rather than fall back on their goto pattern that may or may not fit the problem. Especially junior and mid level engineers might not have good heuristics on is their design any good or is it just following whatever cargo cult they were brought up in.

Like one engineer on my team implemented this crazy overkill logger and I asked a few questions why do it like this and the answer was that they had implemented it in another language at another company. After that I told them to not have more abstraction layers than concrete implementations when adding a new feature.

I think a good programmer should have an "intuition" whether it is worth to build an abstraction for something or not. If in doubt don't do it.

If in hindsight your intuition fooled you constantly, adjust it.

I agree but it's kind of too vague to have as a company/team-wide policy
Sure, but I wouldn't implement something like that as a policy, but as a guideline. So when someone really goes overboard into one or the other directionyou can point them to the guideline, but there is still some freedom in deciding on the spot.
If the need / opportunity to abstract something is highly subjective then it is best left to the team lead / senior architect. For all other obvious cases having a policy as outlined above strikes a healthy balance between autonomy and uniformity.
While I usually like the zero-one-infinity rule as a go to when there aren't any other constraints, when trying to build an abstraction it can be fairly tricky to suss out the parts that actually are share vs what is actually different. Two unique and independent users could share a lot of process &c randomly, 3 is a little less likely.
> designing an abstraction for 3 clients is actually quite complex.

tells how abstract abstraction is(to limited extent)

It has been shown over and over to be a good number for this purpose.

You don't design the abstraction for 3 different clients as often as you abstract it from code used by 3 different clients.

The rule of 3 is catchy like you say, which means programmers will have a better chance of remembering when it's needed.
But a catchy name serves only to be catchy it doesn't serve as justification for the rule actually being correct.
Ye I don't like these way too specific rule of thumbs either. It is superstition that is invoked during code reviews to not having to explain or justify your arbitrary nagging on the reviewing side or defending a bad layout on the other.

Stuff should be analyzed in its context.