Live data from Hacker News

Good refactoring vs. bad refactoring

builder.io

91–100 of 154 posts

Re: Good refactoring vs. bad refactoring

#91
post #5

The first example complained about the refactor appealing to functional thinkers (implying that it would be difficult to grok by the existing devs), but then the “improved” version is virtually the same save for the (unnecessary?) use of Ramda in the first. And while many devs are resistant to try functional ways, this first example reads so much better than the original code that I find it impossible to believe that…

(Raises hand.) I prefer the for loop. Pushing items to an array is idiomatic Javascript for creating an array. An if statement is an idiomatic way to do it conditionally. It's also easier to debug. The map and filter methods are nice too, but they're for one-liners.

I don't code in JS very often, so there's that.

Both sets of code were fine, but I understood the loop variant instantly, while it took me a bit longer with the FP code.

As a side note: The only real JS I did was optimising some performance critical code, and I did have to refactor a number of FP chains back to loops. This was because the FP way keeps constructing a new list for each step which was slow.

Re: Good refactoring vs. bad refactoring

#92

Agreed with everything except the following: >Remember, consistency in your codebase is key. If you need to introduce a new pattern, consider refactoring the entire codebase to use this new pattern, rather than creating one-off inconsistencies. It's often times not practical (or even allowed by management due to "time constraints") to refactor a pattern out of an entire codebase if it's large enough. New patterns can…

The key word is consider. If you wouldn't apply the pattern to the whole codebase, maybe you don't actually want to introduce it in just this one new place.

It's not that it wouldn't be applied to the whole codebase, it's that it wouldn't be applied to the whole codebase __at once__. You have to start somewhere and new features are a good place to start new patterns. Older code can be refactored piece by piece.

Re: Good refactoring vs. bad refactoring

#93
post #60

Earlier quoted context omitted.

Huh, I guess that's true, even on wikipedia. I will have to stop using the word then. Though I'm pretty sure most people use it in day-to-day with much more abandon.

The metaphor, I think, is that your code is a mathematical function, and, to be even more specific and "toy example" about it, let's say it's a polynomial. If the old code was x^2 + x z + y x + y z then you notice that you can express the same polynomial as: (x + y)*(x + z) It's still the same polynomial, but you "separated concerns", turning it into a product of two simpler factors. Similar ideas apply to sets of tu…

If you change the polynomial, you change the order of calculations and hence change behaviour. You might get overflows in case of integers or different precision in case of floats.

Nice to learn where the word originates from. Often the meaning of words change over time. E.g. today no horses need involved in bootstrapping.

Re: Good refactoring vs. bad refactoring

#94

Good refactoring should significantly reduce the size or complexity of a codebase. These two metrics are interrelated, but as a general rule if the gzipped size of the codebase (ignoring comments) does not go down, it's probably not a good refactoring.

I'm going to disagree and see what other people say.

I don't think that reduction of size is of any relevance. I admit my own refractors tend to make things smaller but it's only a tendency. Most definitely some increase the size overall. I'm currently refactoring a code base – for each item there used to be one class. Each object was examined after creation then a runtime flag was set: Rejected or Accepted. As the code crew I found I was wasting a lot of time around this Accepted/Rejected stuff. Now I'm refactoring so I have two classes for each item, one for when it's Accepted and one for when it's Rejected. The amount of boilerplate has definitely bulked up the code but it will be worth it.

As for complexity, I don't know.

The only thing I refactor for is human comprehensibility. That is the final goal. What other goal can there be?

Re: Good refactoring vs. bad refactoring

#95
post #11
post #4

Earlier quoted context omitted.

Aesthetic aside, I am under the impression that people start programming, by and large, with imperative for/if style => so the imperative style is readable by more people. Even for more experienced programmers, reading imperative probably cost less energy, since it is more internalised? Futhermore, in JS, the functionnal style is less performant (nearly twice on my machine, i assume because it do less useless memory…

> I am under the impression that people start programming, by and large, with imperative for/if style => so the imperative style is readable by more people. IMO, this is a simple consequence of technology moving faster than society. There are still instructors out there who learned to program in an environment where the go-to options for imperative programming were C and FORTRAN; the go-to options for other paradigms…

Functional programming is more intuitive for many pure data transformation tasks.

It's not more intuitive for the entire system.

When you make a new file in a file system, you're already violating functional programming, even if you atomically create that file with all the content specified, and make it immutable.

You must construct a new file system which is like the old one, but with that file, and then have the entire system tail call into a world where you pass that new file system as a parameter, so that the old one is not known (garbage).

Unix has been the most successful system in getting partial functional programming into ordinary people's hands.

A Unix pipeline like dest-file is functional except for the part where dest-file is clobbered. Or at least can be functional. The commands can have arguments that are imperative programs (e.g. awk) but the effects are contained.

In the famous duel between Doug McIlroy and Knuth in solving a problem, in which McIlroy wrote a concise Unix script combining a few tools, McIlroy's solution can be identified as functional:

  tr -cs A-Za-z '\n' |
  tr A-Z a-z |
  sort |
  uniq -c |
  sort -rn |
  sed ${1}q
Nowhere is there any goto statement or assignment.

Re: Good refactoring vs. bad refactoring

#96

Earlier quoted context omitted.

The key word is consider. If you wouldn't apply the pattern to the whole codebase, maybe you don't actually want to introduce it in just this one new place.

It's not that it wouldn't be applied to the whole codebase, it's that it wouldn't be applied to the whole codebase __at once__. You have to start somewhere and new features are a good place to start new patterns. Older code can be refactored piece by piece.

I've had success with strategies at introducing some abstractions/patterns at my current place(doing this alone for a enterprise SaaS company with 200-ish devs). It's weird that we don't teach these or talk about them in software engineering(AFAIK). I see them being re-invented all the time.

To borrow from medicine: First step is to always stop the `stop the hemorrhage`, then clean the wound, and then protect the wound(or wounds).

- Add a deprecation marker. In python this can be a decorator, context-manager, or even a magic comment string. This i ideally try to do while first introducing the pattern. It makes searching easier next time.

- Create a linter, with an escape hatch. If you can static analyse, type hint your way; great! In python i will create AST, semgrep or custom ones to catch these but provide a magic string similar to `type: noqa` to ignore existing code. Then there's a way to track and solve offending place. You can make a metric out of it.

- Everything in the system as to have a owner(person, squad, team or dept). Endpoints have owners, async tasks have owners, kafka consumers might have owners, test cases might have owners. So if anything fails you can somehow make these visible into their corresponding SLO dashboards.

  The other alternative to this last step is "if possible" some platform squad can take over and do this as zero-cost refactor for the other product squad. Ofcourse the product squads have to help test/approve etc. It's an easier way to get people to adopt a pattern if you do it for them. But the ROI on the pattern has to be there, and the platform squad does get stuck doing cruft thankless work sometimes. If you do this judiciously the win might be thanks enough, like more robust systems, better observability/traces, less flaky tests etc. etc.

Re: Good refactoring vs. bad refactoring

#97

Earlier quoted context omitted.

The key word is consider. If you wouldn't apply the pattern to the whole codebase, maybe you don't actually want to introduce it in just this one new place.

It's not that it wouldn't be applied to the whole codebase, it's that it wouldn't be applied to the whole codebase __at once__. You have to start somewhere and new features are a good place to start new patterns. Older code can be refactored piece by piece.

In the majority of places I've worked, nobody who started refactoring older code piece by piece ever finished it. The exception is people who documented the scope of the work, got leadership buy-in, and then worked on it continuously like any other project.

The problem is that sometimes the new pattern gets overridden by an even newer pattern, and so on, until you've got three different implementations from 2016, 2019, 2021, and then you find that in 2024 you're working on implementation number four and all the people who did the first three have left the company without writing any documentation or finishing their work.

Re: Good refactoring vs. bad refactoring

#99

Agreed with everything except the following: >Remember, consistency in your codebase is key. If you need to introduce a new pattern, consider refactoring the entire codebase to use this new pattern, rather than creating one-off inconsistencies. It's often times not practical (or even allowed by management due to "time constraints") to refactor a pattern out of an entire codebase if it's large enough. New patterns can…

To me it’s less about “consistency” as some nebulous, subjective thing. If you want to set the new standard for $thing, whole-ass it and set the new standard for $thing. I fully support this, but within reason of course. The point at which I duck out of this is when numerous replacement operations require significant non-trivial changes to highly depended on and/or untested code. Otherwise if it’s a simple task that a good IDE and a couple hours of hard work can solve… just do it!

Re: Good refactoring vs. bad refactoring

#100
post #11
post #4

Earlier quoted context omitted.

Aesthetic aside, I am under the impression that people start programming, by and large, with imperative for/if style => so the imperative style is readable by more people. Even for more experienced programmers, reading imperative probably cost less energy, since it is more internalised? Futhermore, in JS, the functionnal style is less performant (nearly twice on my machine, i assume because it do less useless memory…

> I am under the impression that people start programming, by and large, with imperative for/if style => so the imperative style is readable by more people. IMO, this is a simple consequence of technology moving faster than society. There are still instructors out there who learned to program in an environment where the go-to options for imperative programming were C and FORTRAN; the go-to options for other paradigms…

One problem I have with functional programming is that I find it hard to debug.

With imperative programming you can follow a program step by step, line by line, it can be done with a debugger, pen and paper, or in your head.

With functional programming, not so much. It runs functions. What do these functions do? Don't know, they are pieced up from someplace else in the code. And thanks to lazy evaluation, they may not even exist. In the design phase, it is mostly a good thing, as it is flexible, and pure functions are less likely to make a mess than functions with side effects, but there will be a point where the program will not behave as it should, no matter your paradigm. And that's when it becomes a problem.

It is also a problem with object programming if you abuse abstraction, in fact, it is a general problem with abstraction, but functional programming makes it the default, whereas imperative programming is concrete by default.

As for the Python example, I am a bit surprised that the optimizer didn't catch it, all three are common and equivalent constructs that could have been replaced by the most performant implementation, presumably the third one. But well, optimizers are complicated.

Post reply on HN