Hacker Read top | best | new | newcomments | leaders | about | bookmarklet login

>I don't always need to have n+1 layers in my architecture where all the layers just call the next layer anyway.

This is by far the most common thing I’ve seen consistently in especially difficult to maintain codebases. Anecdotal for sure, but number 2 on that list is way behind. Extra abstractions for a future that has yet to happen and abstractions because the IDE makes it easy to click through the layers is the number 1 by far reason I’ve seen code based be very difficult to maintain.

If you just keep in your head, “Can I see exactly enough on this page to know what it does? Not more, not less?“ It’s an impossible ideal but that concept is a fantastic mental guideline for maintainable codebases.



view as:

> Can I see exactly enough on this page to know what it does? Not more, not less

Is there some book/website/SO post that tries to drive this piont home? Bascialyl some web resource I can link to other programmers to explain the value of coding as such.


This article is from a personal blog on a website with a URL $someguysname.ninja.

Maybe you should be the one to write the article you seek! Believe in yourself. If you find yourself with steadfast values that you find tedious to repeatedly communicate, but that you thinks others ought to know about, why not write them down? Who knows - if it's good and resonates with others, is sounds advice, etc. one day it may end up on HN too.

Not everything worth doing has already been done before!


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.


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?

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.

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.

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.


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.

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.

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.


> designing an abstraction for 3 clients is actually quite complex.

tells how abstract abstraction is(to limited extent)


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)


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.

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).


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.

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.

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.

One of the best products I've had to maintain recently was a cgi app with very few abstractions, many of the pages in the app didn't even have functions, just construct sql, read it and spit out html. If someone had a problem all the code was right there in a single file and the error could be found patched and deployed in minutes.

Over the years there were a couple of attempts at replacing this legacy system with a "well-architected" .net one but all the architecture made things harder to maintain and it only ever got to a fraction of the functionality. When there was a bug in those ones we had to not only find it but we had to go through every other bit of calling code to ensure there were no unwanted side effects because everything was tied together. Often the bug was in some complicated dependency because spitting out html or connecting to a database wasn't enterprisy enough. Deployment was complicated enough it had to be done overnight because the .net world has a fetish for physically separating tiers even though it makes many things less scalable.

90% of the corporate/enterprise code I've seen would be much better off being more like that cgi app.


Counterpoint - code like that is OK if the project is small and tidy, but over a certain size, changes become horrible refactoring efforts and adding multiple developers to the mix compounds the problem. The 'enterprisey' rework that you describe sounds badly architected, rather than an example of why architecture is bad. Good architecture is hard to do but I don't agree that means we're better off not bothering.

Incidentally one of the biggest benefits I see of using a text editor like vim / emacs is that it really encourages good code management.

It's not to save the ~10 minutes per year in faster key strokes to manipulate your code. It's about the way it shapes your thinking about how you code.


I agree to some extent.

After using Intellij for about 5 years I switched to a less batteries-included code editor (currently doom emacs). I figure if I need an IDE to navigate our code as a senior developer on the project then less experienced ones don't stand much of a chance.

I still use Intellij for refactoring.


> Extra abstractions for a future that has yet to happen and abstractions because the IDE makes it easy to click through the layers is the number 1 by far reason I’ve seen code based be very difficult to maintain.

This is always tempting. A good argument against it is to realise that future developers (us included!) will know their requirements far better than we can guess them; if code needs writing, they should do it (as an extra bonus, we don't waste effort on things which aren't needed). The best way to help them is to avoid introducing unnecessary restrictions.


> The best way to help them is to avoid introducing unnecessary restrictions.

But that's the other side of the exact same coin. How do you know if a restriction today is good or bad for the future? Restrictions prevent misuse and unexpected behavior, in the good case.


Without a doubt this is my biggest issue within the software industry. Massive amounts of indirection & abstraction under the guise of 'DRY'.

My first programming job was with a firm that never had money for paying developers, let alone tools. It was also a few years before Visual Studio Code was a serious thing. So I used "programmer's editors" -- those cute things like Notepad++ which had syntax highlighting and on some days autocomplete but no real code understanding. There was no middleware, no dependency-injection, and things like the database instance were globals. More or less, the things you needed to know were in a single file or could be inferred from a common-libraries file.

My second job, they splashed the cash for full-scale professional IDEs, and they couldn't get enough abstraction. I suspect the conveninence of "oh, the tools will let us control-click our way to that class buried on the opposite side of the filesystem" made it feasible.

I wonder if there's some sort of "defeatured" mode for IDEs which could remind people of the cognitive cost of these choices.


Legal | privacy