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

I've noticed what I'd term a "post-modern" sensibility towards code quality, where some of the often cited tenets of good code are being critiqued or questioned. Stuff like yes, you can have super large functions or super large files. Or yes, you can have repetitive code. Turns out a lot of this stuff doesn't impact readability or in fact is more readable than the alternative. I'd much rather read one function top to bottom than have to jump into 7 nested function calls that are only called once. And I certainly don't need the levels of abstraction that design patterns or Haskell typeclasses demand.

There's a real danger in a well-intentioned clean code advocate. First, because there are no universal code principles. Maybe "have it work", but even then, a lot of value has been gotten out of code that doesn't work. And second, because it's very easy to go from a good guideline to a dogma. All you need is a few people to misinterpret your guidelines. Heck, a lot of people haven't even read the guidelines. They just parrot what someone else paraphrased on Twitter.



One opinion that seems to have completely changed since I first learned is about having a single `return` in my function. It was hammered into my when I first learned that you should only ever have one exit in your code, anything else is bad practice.

Fast forward about 11 years, and people realize that having a million nested if statements and matching else blocks is actually absurdly unreadable; much better to just return early, avoid nesting, and break the "only one exit" pattern.


I'm big fan of separating functions into validation and logic using early returns:

  function doSomeStuff(input) {
      if(someInvariantIsUnsatisfied(input)) {
          return Error;
      }
      if(someOtherInvariantIsUnsatisfied(input)) {
          return Error;
      }

      //do stuff

      return result;
   }


The term for that is a "guard". Swift took it literally and introduced guard statements which force you to terminate the containing scope (return, throw, continue, break) or call a function that is marked as never returning. If control flow can ~~enter~~ exit the body of the guard statement without hitting one of those, it's a compiler error.

EDIT: Exit the body of the guard statement, not enter.


> ~~enter~~

I get you’re going for the non-standard markdown strike through, but that looks confusing and calls attention to something you meant to remove. You can (well, could) remove the word when editing.


It is possibly the one Markdown feature I'd like HN to have. I use it all the time on Reddit to correct my comments without making replies look like nonsense.


You can use unicode, l̶i̶k̶e̶ ̶t̶h̶i̶s̶


I find this the absolute most readable way to write defensive code. Huge fan.


I think that dates back to assembly where you have to clear up a stack frame before returning.

Especially when the code to do that depends on the amount of locals, having only a single place where you assign space for locals and a single one where you revert that helps a lot.

It also can help in languages that don’t help you run cleanup code (closing files, freeing memory) at function exit. Change the code and forget to update one of your early exits, and you have a bug. If that exit is rare, that may ship and may become a vulnerability.


Additionally, something that is forgotten now that structured programming won so hard, and so little assembly is hand-written, is that it's possible to return to different places. As in, in case A return to address F, but in case B return to address G.


The Linux kernel still uses centralized exits in places, with goto to do cleanup.

https://github.com/torvalds/linux/blob/master/Documentation/...


At my first programming job they were religious about the single `return' and followed a pattern that was bizarre and luckily I've not seen since--using a `retVal' variable declared at the top.

  int foo(int bar) {
    int retVal = -1;
    if (bar == 1) {
      retVal = 1;
    }
    if ((retVal % 2) == 0) {
      retVal = 2; 
    }
    return retVal;
   }


Yes, I've seen this a lot over the years.

Also, putting code inside a do ... while( false ) loop purely so you can use break as a sneaky goto to avoid deeply nested conditionals while technically adhering to the single return rule.

I think when you're starting to use control statements in bizarre ways like that, it's a good indication that maybe that's a case where breaking the "rules" is the best thing.


It might have improved in the last few years, but as of 2018, MSVC would not do named value return optimization for the case where all branches returned the same local variable, in my experience. There had to be just the one penultimate return statement, in order for NRVO to kick in.


Most of my teammates are in Serbia and I see naming the return object "toReturn" a lot... so yes, the code reads "return toReturn;"


My biggest pet peeves in the code I'm currently working. It leads to multiple multi-line ternary to keep a single return.

Single Return made sense when we had to de-allocate resources, but there are few years already that most languages have memory safe resource-counted allocations (C++11 for i.e) that will free resources correctly independent of your single/multiple returns.

I don't believe that keeping those inherited "best practices" from the past will help us developing modern code.

Long life to Clean Coding!


> It was hammered into my when I first learned that you should only ever have one exit in your code, anything else is bad practice.

Yes. This rule was an overreaction to a pervasive problem back in the day.

But, much like the use of "goto"s, there are times where your code is more readable, maintainable, and efficient if you ignore the prohibition. The trick is to know when that's the case.


I see it more as a tick-tock pendulum swing. A principle is neglected, then comes roaring back because of the glaring need for it. It becomes overused, fetishized, and popularized in warped ways. Then the "accepted design principle considered harmful" articles appear, and the pendulum starts to swing back the other way.

What we have to keep in mind is that the needs of whatever codebase we're working on might be out of sync with fashion. For example, I've been relishing the recent push-back against DRY, because I think it's over-applied in damaging ways, but the main codebase I've been working on for the last year needs more application of DRY, not less.


> And I certainly don't need the levels of abstraction that design patterns or Haskell typeclasses demand.

I think it's fair to say that AbstractSingletonProxyFactoryBean[0] is something of an abomination, but it's important to avoid throwing the baby out with the bathwater. Some of the Go4 patterns like Command, Observer, Visitor, and Iterator are literally all over the place and you have to actively go out of your way to avoid writing them.

Likewise, Haskell type classes aren't something you're supposed to be implementing yourself all over the place in your application. They're very high bang-for-buck things you implement in libraries. The equivalent to implementing iterators for your collections, or AutoCloseable on resource-like java classes.

0. https://docs.spring.io/spring-framework/docs/current/javadoc...


> I'd much rather read one function top to bottom than have to jump into 7 nested function calls that are only called once. And I certainly don't need the levels of abstraction that design patterns or Haskell typeclasses demand.

I’d say it’s pretty subjective. I’m the opposite. I can’t read functions that are a hundred lines long with a dozen bound variables and half a dozen branches and early exits. That’s not easy to read and it requires keeping a notepad with you to figure out the control flow.

I much prefer short definitions with clear logic. Leave the semantics to the edges of the program where it matters. It’s easier to reason about using substitution and algebraic manipulation.

Don’t get me wrong though, I write my fair share of C code and low level drivers. For that though I tend to use a higher level language to work out the logic.

My business isn’t writing code. That’s the boring part. It’s solving the problems that is interesting.

But that’s the funny thing. Regardless we could both solve the same problems in our own ways and more often than not it will be good enough.

After twenty years and hoping for another twenty… I guess I’ve learned that what matters is that you can get along with your team and read each other’s code. Styles come and go and don’t matter that much.


That’s not easy to read and it requires keeping a notepad with you to figure out the control flow.

It does require a notepad as well when a five-page long function gets smeared across three “services”, five files and tens of functions, each doing a couple lines of code.

Leave the semantics to the edges of the program where it matters

Agreed. But when I meet this highly-structured code (and get confused), it’s usually split not across semantics, but across some random undocumented ideas about reuse, undeclared abstractions, highly indirect flow control, etc. And all of that named as if it was a most bizarre naming contest.

So that instead of a little hairy idea you get a convoluted irrational multi-plane horror to deal with.


Yes, I understand. In highly procedural code the unprincipled combination of functions results in layers of indirection which do not make the code any easier to understand.

It requires discipline to achieve what I call here, principled combination: that the combination of functions follow certain laws like they do in mathematics. In many programming languages it requires discipline because the type systems and the programming languages are not constructed in such a way as to uphold these laws for you. You, the programmer, have to enforce these laws on your code.

That is why, when I'm writing something in C, I will some times prototype my design in a higher-level language first and translate that into C code. The higher level language is more principled and helps me catch errors in the design and development stage before I've made a mess of things.

I don't always do it that way but it tends to work the best when I can get it to.


Yeah, I used to do that (not always) too. Either prototypes or high-level design documents full of pseudocode and interoperating modules. But what kills the whole idea is that our tools do not support such structures in any way. They are completely blind to this.

People praise typed languages, but honestly I’m much more okay with dynamic typing than with the lack of beams and pillars for design, to do any of it. It’s very easy to lose yourself for an hour and start creating a mess in what you have planned for weeks, simply because your attention was elsewhere, there was a deadline or new goals were incompatible with current ideas and you had no clue how to marry them correctly (nor courage to begin) within a timeframe available.

Add a couple of developers who aren’t you and/or switch into another project for a month or two – and it’s a recipe for a mudball. I used to believe in all that, not anymore. May be a bold claim, but most projects aren’t even hard enough to require it. I remember my own past experience and can tell that all that division into layers, concerns, etc was not a requirement nor an enhancement. I was simply ticking boxes in a “how clever do you feel this week” form. Days of work to spare five pages of clear actual instructions which could have been typed and tested in under few hours, shipped next evening and – much more importantly – I could reenter that context in few minutes after a year off, in contrast to any to-be-mudball.


I'm with you re. short functions. Setting boundaries around code, i.e. scope, and giving that behaviour a well-defined signature (i.e. a descriptive name, what it needs, and what it gives back) makes it easier to reason about the overall functionality. Giant functions reduce my confidence that any given change isn't going to introduce a problem.

I like to imagine each execution path through a function as a piece of string. The fewer strings, the fewer kinks, and the less string overall, the easier it is for my monkey brain to handle most of the time. Yes this is 'cyclomatic complexity', but strings are nicer visually :)


> And second, because it's very easy to go from a good guideline to a dogma. All you need is a few people to misinterpret your guidelines. Heck, a lot of people haven't even read the guidelines. They just parrot what someone else paraphrased on Twitter.

Another issue that's never explicitely mentionned is the vastly different calibers of programmers and engineers in the field.

I'm pretty sure John Carmack, Linus Torvald or Donald Knuth's definition of "good code" is quite different than $BODY_SHOP_RESSOURCE_1389's.


> I'd much rather read one function top to bottom than have to jump into 7 nested function calls that are only called once.

I actually would not. I do not want to have to read every detail of each condition or formula on the first pass. I want to see high level gist of what is supposed to happen - high level algorithm. Plus, I like when variables have clearly limited scopes.


I personally take a more pragmatic approach, though perhaps its "post-modern". How well-architected, tested, groomed, etc my code tends to be is proportional to the confidence the business has in the problem space.

If I have a detailed, firm spec that I know won't change at all, I'd spend some time planning and then build a DRYed up, abstracted, well-tested, etc solution. I know the solution is going to be sticking around for a while, more or less, so it makes sense to devote the effort to build a solid foundation.

If the thing I'm building is squishy, looking for heavy user feedback, will likely be iterated on quickly (i.e., very agile manifesto agile), I won't spend the overhead time for those things. There's a nonzero chance the code ends up in the trashcan, and the most important thing is to ship something and get feedback from real customers. You later go back and clean it up, based on the growing confidence in the permanence of that feature.


We have a tendency sometimes to prematurely abstract pieces of code.

I am okay with copy-pasting code but after the second or third copy-paste, you should probably at least consider asking, “Should I abstract this into its own function?”


One way this is formulated is the "Rule of 3 for Abstractions":

One copy of something is YAGNI. You aren't going to need it (an abstraction).

Two copies of something are coincidence. It's fine to copy and paste and leave it that way.

Three copies of something are finally a pattern. At least three copies are when you start to really see what sort of abstraction that you need to handle the pattern.


>I'd much rather read one function top to bottom than have to jump into 7 nested function calls that are only called once.

Well, yeah, coz that's worse.

It decreases code cohesion (especially when those functions are stuck in files far away) and to little appreciable benefit.

>First, because there are no universal code principles

I think there are and good developers have a spidey sense about what they are and will usually agree but we've yet to culturally agree on what they are as an industry.

Moreover, some literature on the topic (e.g. Robert Martin) is very, very wrong.


For some people esthetic considerations, philosophical considerations trumps usefulness, functionality, productivity the ease of coding, the ease of reading and understanding code.

They rather have clean, SOLID, DRY, design patterns than easy, fast, productive code.




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

Search: