Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

It's important to remember that all best practices are not created equal. I'd prioritize readability over DRY. I'd prioritize cohesion over extensibility. When people talk about best practices, they don't talk about how a lot of them are incompatible, or at least at odds with each other. Writing code is about choosing the best practices you want to prioritize as much as it's about avoiding bad practices.


Readability is almost always (almost only because there are some rare exceptions) the most important thing to me, even for low-level systems software. I always ask myself, “If I don’t touch this code for a year and then come back to it, how long will it take me to understand it again? How long will it take someone who’s never been exposed to this code to understand it?”

Luckily, our compilers and interpreters have gotten so good and advanced that, in 95%+ of cases, we need not make premature “optimizations” (or introduce hierarchies of “design patterns”) that sacrifice readability for speed or code size.


Was reading 1978 Elements of Programming Style a while ago. It's mostly Fortran and PL/I. Some of it is outdated, but a lot applies today as well. See e.g. https://en.wikipedia.org/wiki/The_Elements_of_Programming_St...

They actually have a Fortran example of "optimized" code that's quite difficult to follow, but allegedly faster according to the comments. But they rewrote it to be more readable and ... turns out that's actually faster!

So this already applied even on 197something hardware. Also reminds me about this quote about early development of Unix and C:

"Dennis Ritchie encouraged modularity by telling all and sundry that function calls were really, really cheap in C. Everybody started writing small functions and modularizing. Years later we found out that function calls were still expensive on the PDP-11, and VAX code was often spending 50% of its time in the CALLS instruction. Dennis had lied to us! But it was too late; we were all hooked..."

And Knuth's "premature optimisation is the root of all evil" quote is also decades old by now.

Kind of interesting we've been fighting this battle for over 50 years now :-/

(It should go without saying there are exceptions, and cases where you do need to optimize the shit out of things, after having proven that performance may be an issue. Also at scale "5% faster" can mean "need 5% less servers", which can translate to millions/dollars saved per year – "programmers are more expensive than computers" is another maxim that doesn't always hold true).


> Dennis Ritchie encouraged modularity by telling all and sundry that function calls were really, really cheap in C.

The old salty professor who taught numerical physics at my uni insisted that function calls were slow and that it was better to write everything in main. He gave all his examples in Fortran 77. This was in the 2010s...


In fact he is right. The advantage of writing modular code, however, is that we can test the locations where performance is needed and optimize later. With a big main it becomes very hard to do anything complex.


This is why I liked it when the language I was coding in supported inline expansion: I could keep my code modular but nevertheless avoid the penality of function calls in performance critical functions in the compiled code.


> In fact he is right.

Was he, though? I mean, yeah having to push and pop a call stack does indeed require more work than not having to do that. However, compilers can and do inline and optimize out function calls.

And what's the real performance impact of calling functions a constant number of times outside of the hot path? Is an untestable spaghetti salad of things better than a few hypothetical push and pops?

There's wisdom behind Knuth's remarks on premature optimization.


The one gotcha with optimizing for “readability” is that at least to some extent it’s a metric that is in the eye of the beholder. Over the years I’ve seen far too many wars over readability during code review when really people were arguing about what seemed readable *to them*


This is the reason I refuse to use the word "clean" to describe code anymore. It's completely subjective, and far too many times I've seen two people claim that their preferred way of doing things is better because it's "clean", and the other's way is worse because it's "less clean", no further justification added. It's absolutely pointless.


There are a lot of topics in software development where everyone can agree that X is correct. However, *defining* X gets into subjective arguments. And yep, readability and clean code are both in that category.


"Read" isn't quite the right word for code. "Decode" is better. We have to read to decode, but decoding is far less linear than reading narrative text. Being DRY usually makes decoding easier, not harder, because it makes the logic more cohesive. If I know you only fromajulate blivers in one place I don't have to decode elsewhere.


I was just mulling this over today. DRY = easier-to-decode is probably true if you're working on groking the system at large. If you just want to peak in at something specific quickly, DRY code can be painful.

I wanted to see what compile flags were used by guix when compiling emacs. `guix edit emacs-next` brings up a file with nested definitions on top of the base package. I had to trust my working memory to unnest the definitions and track which compile flags are being added or removed. https://git.savannah.gnu.org/cgit/guix.git/tree/gnu/packages...

It'd be more error prone to have each package using redundant base information, but I would have decoded what I was after a lot faster.

Separately, there was a bug in some software aggregating cifti file values into tab separated values. But because any cifti->tsv conversion was generalized, it was too opaque for me to identify and patch myself as a drive-by contributor. https://github.com/PennLINC/xcp_d/issues/1170 to https://github.com/PennLINC/xcp_d/pull/1175/files#diff-76920...


Bazel solves this exact problem (coming from its macrosystem) by allowing you to ask for what I term the "macroexpanded" BUILD definition using `bazel query --output=build //some/pkg/or:target`. When bazel does this, it also comments the file, macro,and line number the expanded content came from for each block.

This gives us reuse without obscuring the real definition.

I automated this in my emacs to be able to "macroexpand" the current buid file in a new buffer. It saves me a lot of time.


Does it? Every time I see DRY'd code, it usually makes the project it's in more difficult to understand. It's harder to understand where values come from, where values are changed, what parts of the codebase affect what. And that's before trying to figure out where to change something in the right place, because it's often unclear what other parts of the code are coupled to it through all the abstractions.

At a high level, at first glance, the code might look good and it "makes sense". But once you want to understand what's happening and why, you're jumping through five different classes, two dozen methods and you still don't know for sure until you run a test request against the API and see what shows up where in the debugger. And you realize your initial glimpse of understanding was just window dressing and actually nothing makes sense unless you understand every level of the abstractions being used.

It's suddenly a puzzle to understand another software developer instead of software engineering.


An IDE can help a lot. Coming from Perl, everything you said was true. I wanted everything in one file as much as possible, and breaking tasks off into functions just meant I had to jump around to try and rebuild the flow in my head. I spent so much time inside the debugger since reading the code would only go so far.

Now I work in C#, we have a lot of classes with a few functions, a lot of helper functions. Doesn't matter since it's so easy to use the tooling to build a mental picture - let alone refactor it in an instant if that variable name feels a bit off, or we think a function is not used (such things were always a risky exercise in Perl).

We refactored one insurance based project to use generic base classes extensively since all insurance shares some attributes and features - this really helped cut down complexity of changes and overall just reduced code on the screen to sift through. I had a lot of fun doing this, I'm a weirdo who almost likes deleting code more than writing it. Once you hit the lowest level it is a little less intuitive due to being generic but at the higher levels we mostly work at, it's simpler, and rolling out a new product we get a lot of stuff for free. They got a long way copy-pasting the product logic (4 or 5 product lines) but at this point it made sense to revisit, and I sneak a bit more in each time I have a change to do.


> Being DRY usually makes decoding easier, not harder

"Usually" being the keyword and what the article is all about IMHO. I work in a codebase so DRY that it takes digging through dozens of files to figure out what one constant string will be composed as. It would have been simpler to simply write it out, ain't nobody going to figure out OCM_CON_PACK + OCM_WK_MAN means at a glance.


Function calls, the essence of DRY, are only readable if it is well known and well understood what it does.

When code is serial, with comment blocks to point out different sections, it is much easier to read, follow, and debug.

This is also a little bit of a tooling problem


>I work in a codebase so DRY that it takes digging through dozens of files to figure out what one constant string will be composed as.

I don't know the codebase, but to my mind that level of abstraction means it's a system-critical string that justifies the work it takes to find.


Sorry, but this doesn't make sense. Why should system critical things be more difficult to understand? Surely you want to reduce room for error, not increase it?


I mean, sure, I guess API urls could be system-critical. But generally, I prefer to grep a codebase for a url pattern and find the controller immediately. Instead, you have to dig through layers of strings composed of other strings and figure it out. Then at the end, you’re probably wrong.


Well, "read" is still the verb we use most often to describe a human interpreting code. Also, many information-dense books are not intended to be read linearly, yet we still say we're "reading" (or "studying") the book.


One area I find DRY particularly annoying is when people overly abstract Typescript types. Instead of a plain interface with a few properties, you end up with a bunch of mushed together props like { thing: boolean } & Pick<MyOtherObj, 'bar' | 'baz'} & Omit<BaseObj, 'stuff'> instead of a few duplicated but easily readable interfaces:

interface MyProps { thing: boolean; bar: string; baz: string; stuff: string; }


Am I crazy for almost exclusively just using type and sum types and no generics or interfaces and somehow being able to express everything I need to express?

Kind of wondering what I'm missing now.


Hmm, you can do pretty nice things with generics to make some things impossible (or at least fail on compile), but I agree it’s hardly readable. In some cases you need that though.


Visually parse.


DRY is IMHO a maintenance thing.

If "I don't want to maintain three copies of this" is your reaction unifying likely makes sense.

But that assumes the maintenance would be similar which is obviously a big assumption.


DRY often gives you the wrong or a leaky abstraction and creates dependencies between sometimes unrelated pieces of code. It’s got tradeoffs rather than being a silver bullet for improving codebases.

Having 0% DRY is probably bad, having 100% DRY is probably unhinged


> sometimes unrelated pieces

you are using it wrong

https://news.ycombinator.com/item?id=40525064#40525690


I agree and would add that one of the goals for technical design or architecture work is to choose the architecture that minimizes the friction between best practices. For example if you architecture makes cohesion decrease readability too much then perhaps there is a better architecture. I see this tradeoff pop up from time to time at my work for example when we deal with features that support multiple "flavors" of the same data model, then we have either a bunch of functions for each providing extensibility or a messy root function that provides cohesion. At the end both best practices can be supported by using an interface (or similar construct depending on the language) in which cohesion is provided by logic that only cares about the interface and extensibility is provided by having the right interface (offload details to the specific implementations)


I have a pessimistic view that ultimately the only best practices that matter are the ones your boss or your tech lead likes.


What about when you are the boss or tech lead?


Then the only best practices that matter are the ones that your team believes are correct


The best practices are the ones that allow you to do business and where the maintenance work is relatively not too painful considering the budgeted development time.

Your task is to deliver a good product, not necessarily good code.


> The best practices are the ones that allow you to do business and where the maintenance work is relatively not too painful considering the budgeted development time.

the problem is even that in concrete terms can be controversial. everyone wants to minimize maintenance work; not everyone agrees on what kind of code will achieve that.


Agree and would add that software projects also run through different phases in their lifespans with each phase having their own objectives [1].

So while - as you say - best practices can be at odds with each other - dev teams might be following both over time, just prioritizing one in some phase while completely disregarding it during another.

[1] E.g. the UI of the actual product might pivot multiple times at phase 1 because the product has yet to find its niche or core offering. While at a later stage the focus might be on massive scaling, either in numbers of devs or rolling out the product in new jurisdictions. Other phases might be a maintenance one, when an "offshore" team is given ownership or a sundown of an application.


Maintenance is 90% of a project life time. Sometime those "best practices" rigid implemented means the project won't live to see even it's 1st birthday.


> Cohesion over extensibility.

Imo the very best approach is a codebase that's small enough that you can just do chunky refactors every so often rather than building in extensibility as a "thing". Not applicable to all problem spaces (I'd hate to do this for UI code), but for a lot of stuff it works really nicely.

For me this often looks like an external DSL/API that stays relatively constant (but improving), with guts that are always changing.


> I'd prioritize readability over DRY.

Yes. Especially at the beginning when it's critical to ensure that the logic is correct.

You can then go back and DRY it up while making sure your unit tests (you did write those, right?) still pass.

PS: same applies to "fancy" snippets that save you a few lines; write it the "long way" first and then make it fancy once you're sure it runs the way it's supposed to


> You can then go back and DRY it up while making sure your unit tests (you did write those, right?) still pass.

not gonna happen once merged


I am of opinion, code should be written to be readable. Rest of the desirable properties are just side-effects.


Most commonly, code should optimized into being easy to change.

That's almost entirely coincidental with being easy to read. But even easiness to read is a side effect.


I agree with this. Easy to change often means good tests too.

I worked in Perl. Yes it has a reputation for being hard to read, but that was not the problem. Our scripting was pretty basic and easy to read. It's the loose typing, the runtime evals, the lack of strict function parameters, no real IDE, “Only perl can parse Perl” - the fact you can load a module from a network share at runtime, import it, and call a function, based on a certain run flag - and so on. Refactoring was always a mine field and there was a lot I wanted to do in my old job but could not justify it due to the risk.


Fully agree. I think this is something that takes some time/experience to appreciate though. Junior engineers will spend countless hours writing pages of code that align with the “design patterns” or “best practices” of the day when there’s a simpler implementation of the code they’re writing. (I’m not saying this condescendingly—I was once a junior engineer who did that too!)


It’s impossible to know what “good” looks like when you’re new and haven’t seen a few codebases of varying quality and made some terrible mistakes


I think it's fair to say that between behavior and maintainability, one is inflexible and the other hangs from it in tension.


"Side effects" are not the same as "less important traits."

Side effects are usually unrelated or unwanted.


aka "Engineering is about trade-offs"


I place copy-pastability somewhere into those priorities too :)


Readability doesn't matter much when you have 10,000+ lines of code. You aren't going to read all that code, and new code introduced by other people continuously isn't something you can keep track of, so even if you understand one tiny bit of code, you won't know about the rest. You need a system of code management (documentation, diagram, IDE, tests, etc), to explain in a human-friendly way what the hell is going on. Small chunks of code will be readable enough, and the code management systems will help you understand how it relates to other code.


> Readability doesn't matter much when you have 10,000+ lines of code. You aren't going to read all that code (...)

You got it entirely backwards. Readability becomes far more important with the size of your project.

When you get a bug report of a feature request, you need to dive into the code and update the relevant bits. With big projects, odds are you will need to change bits of the code you never knew they existed. The only way that's possible is if the code is clear and it's easy to sift through, understand, and follow.

> You need a system of code management (documentation, diagram, IDE, tests, etc), to explain in a human-friendly way what the hell is going on.

That system of code management is the code itself. Any IDE supports searching for references, jump to definitions, see inheritance chains, etc. Readable code is code that is easy to navigate and whose changes are obvious.


> Readability doesn't matter much when you have 10,000+ lines of code. You aren't going to read all that code,

As someone who has read 10,000+ lines in order to track down surprising behavior in other people's code, I can say without a doubt that readability still matters at that scale.

Code management systems can sometimes be helpful, but they are no substitute.


> Small chunks of code will be readable enough

Ravioli code is a real problem though. Saying small chunks are readable is not enough. The blast radius of a five byte change can be fifteen code paths and five million requests per hour.


10KLoC is a very small app. Ours isn't that big and it's 140KLoC and I have read almost all of it.


To be fair, not all lines of code are equal. A project with a state machine, commands, strategy patterns, etc requires an awful lot of repetitive boilerplate.

A number-crunching app or a data processing pipeline packed with spaghetti business logic is far harder ti read.


And that is why KLoC is a very piss poor metric.

>A project with a state machine, commands, strategy patterns, etc requires an awful lot of repetitive boilerplate.

Yeah, good thing we're not a java shop...


Even if you’re not going to read 10.000+ lines, if the few you read are easy to understand you’re still going to have a much better time maintaining the codebase.


> You need a system of code management (documentation, diagram, IDE, tests, etc), to explain in a human-friendly way what the hell is going on

I think this is where AI could be helpful in explaining and inspecting large codebases, as an assist to a developer.


Maybe but hallucinations become a real problem here. Even with publicly available API's that are just slightly off the beaten path, I've gotten full-on hallucinations that have derailed me and wasted time.


> I think this is where AI could be helpful in explaining and inspecting large codebases, as an assist to a developer.

That's a great point. Everyone lauds the benefits of chatgpt/copilot in generating new code, but I'm starting to learn that the places they might shine is onboarding onto projects and preliminary code reviews. What LlMs excel at is context, and they should excel in activities where context-awareness is key.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: