Do you think refactoring code adds value?

Pick one:

1. Hard No (Only refactor when there is a dollar value associated with it, i.e new feature depends on it).
2. Somewhat Yes (Futureproof your code, anticipate easiness to build feature requested in future).
3. Yes (Developer happiness, retention and for point 2)

  • 13
    Refactor it if you touch it.

    I wouldn't go far out of your way to refactor an entire class or file (depending on your style) but as you make changes to a method, see if there's a better way of doing what it's doing now, languages typically evolve and so should the code base as you change something.
  • 3
    Probably 2. It adds value down the line, when project changes or scales. It will add value by reducing number of bugs and save you time.

    Also 3, but for different reasons. Time and complexity required for refactoring later on can be magnitude greater. We all know what answer to expect, when we ask "can we dedicate a week or two for refactoring".
  • 5
    I’d say 2, but only if you have reliable unit tests or a good QA person that can regression test. All too often you refactor something that is barely used and poorly understood.
  • 1
    @WildOrangutan makes a great point, refactor whenever you can possibly persuade business to tolerate the expense because when they're squeezing to meet quarterly goals no argument in the world will convince them.
  • 8

    Not only because I love refactoring. But also because it keeps the code easy to maintain and extend for future features.

    My personal question:
    Are there devs who like refactoring without a compiled language? Because that sounds like a nightmare to me.
  • 5
    Also, remember that a software solution isn't just present in the world as a set of features. It also has bugs, vulnerabilities, and consumes computational resources at the very least. The nature of bad code is that you don't know what problems it has until you unravel it, so refactoring carries the potential of fixing bugs - maybe even known bugs on the backlog - , preventing future security incidents, and sometimes even saving on compute expenses, although performance problems can be detected without understanding the code that's causing them.
  • 4
    @Lensflare I like refactoring Typescript, and correctly type-annotated Python and Elixir. It's not about compilation. Actually, I like refactoring these languages even if they don't have type annotations, because adding them is an easy step that improves the quality massively.
  • 1
    @lorentz if it’s not about compilation, how do you know if you did a mistake?

    I count transpiling from TS to JS as compilation for the sake of this argument.
  • 2
    3 definitely, but only when ur not about to get fired lol
  • 1
    @Lensflare In the case of Elixir, the runtime tells me. In the case of Python the IDE tells me. inexplicably the standard dictates that a Python implementation MUST NOT report type errors, and MUST NOT report syntax errors within functions if found in advance, but rather throw a runtime error when the erratic line is reached. I'm not a fan of this rule, but I do have an IDE and a language server so it doesn't hinder me much.
  • 3
    @lorentz when you say Python and IDE, do you mean static code analysis? Didn’t know Python has this.
    Maybe I picked the wrong term when I said compiled languages.

    I actually mean languages capable of being statically analyzed.
    Maybe this is too vague, too, I don’t know.

    I just rely on the compile time errors so much when refactoring, that I can’t imagine what it would look like without that stuff. Actually, I can imagine it, and it’s horrible 😆

    But now I can’t pinpoint what it is exactly that makes refactoring a joy with compiled languages. It’s kind of the capability to be statically analyzed and checked, in a very strict way.

    And about Elixir: You say the runtime does this. Does it mean you have to run your program repeatedly in the middle of refactoring? That doesn’t sound fun ^^
  • 1
    Just had a difficult bug to fix where I found that the code was badly partitioned and that most of the bug would probably just go away with reorganising it. Indeed, there was just one if condition missing afterwards, easy fix.
  • 0
    @Lensflare I think ElixirLS calls the runtime with some special arguments, but this is pretty much inevitable since macros can generate types.
  • 0
    @lorentz Afaik, Rust can generate types with macros, too. But at compile time.
  • 0
    @Lensflare Python has type annotations but the runtime ignores them. The language server warns you if they don't match, but it doesn't do anything if type checking is inconclusive. It's a very underdeveloped type system but most languages support this degree of static analysis. Ruby, PHP, you name it. If you don't want to use Typescript, JSDoc can also be checked. I highlighted Python because it has a "don't try to be too clever" policy which means that the majority of code that predates type annotations can still be correctly annotated.
  • 0
    @Lensflare Well yeah, because Rust has a separate compile time. Languages that decode syntax before execution have to squeeze type checking between the two so it ends up in the field of responsibilities of the runtime which does both decoding and execution.
  • 0
    @lorentz how far do those type annotations go? Do you get warnings if you access a renamed or removed variable or function? Or what about unused variables? Or switch cases becoming not exhaustive when an enum gets some extra cases?
  • 0
    @lorentz How does that runtime stuff help with refactoring then? I still don’t get it. You change something, then you run your code to see if it breaks? What guarantees that the part of the code will be executed to see if it breaks?
  • 1
    @Lensflare Exhaustiveness checking is very new and rare, and it contradicts custom matchers which I really miss from Rust. I should be able to match behind an Rc.
    For the others, it varies, some of these are impossible in some dynamic languages, invalid names are usually detected though.
  • 1
    @Lensflare Static type checking done by the runtime, where it's present, is still done at startup, but in any case, the only thing that matters to me is that the code contains types and therefore a language server can produce type errors in the IDE on the lines I'd closed even while I'm typing the rest of the code.
  • 1
    @Lensflare any language can be refactored and it gives the same good feeling for me all the way from HTML to C++ to Assembly.
  • 1

    I have worked in multiple legacy products and the struggle is real (PHP). The insane implementations people come up with... Every bit helps to reduce the sheer mental overhead in these systems.

    Refactoring then is not so much about future-proofing for new features, but DX and product stability. Adding tests can be near impossible, but every little improvement helps!

    Boy scouts rule ftw 😎

    (Overall, you can modify implementations details "on the side", but not the architecture. Bigger tasks like moving a monolith to Docker takes time and must be planned like any other ticket.)
  • 5
    Hard yes! Can't be squeezed in your 1,2,3 though. Your number 2. is a slippery slope to premature optimization. Only do that when it's on the roadmap or makes the code/architecture simpler (to understand).

    @C0D4 makes a good point when you touch it. Sometimes you have to because it doesn't allow for what you are building. But you can hack it in to make it work. Keep doing that and you rack up tech debt.

    Refactoring is a means to remove tech dept. So a lot of reasons are related to it. The big two are
    1. Prevent/fix bugs
    2. Develop faster

    Example 1: function has to return a striped version of a data structure in a delete event. Before refactoring it was building the structure twice. So when adding a property that is needed in both the delete and update events it was missed in the delete event. After refactor the base structure is always created and for updates other stuff is added. Making it impossible to create this bug.

    Example 2: validations of an entity are done in 4 layers of code (including database constraints). Adding a new field or validation requires to figure out where it works without conflicting with the validation/error reporting elsewhere. 30 min job can take half a day if placed wrong, especially for people who are unfamiliar with the codebase struggle to find the right place and have to learn 4 different concepts. After refactoring there is a single validation framework + db constraints as backup for programming mistakes. Adding a variation requires less code and is simple to understand.
  • 0
    @Lensflare to answer your question. Yes I enjoy it and it's not bad at all! I've done this with PHP and Python. I get the same satisfaction. Especially since PHP 7 that introduced typehints. Just adding those exposes some weird behaviour (multiple types of input or retuned). Not having tests though makes refactoring a nightmare. Even when you make big changes or additions the tests document the intended behaviour and you are deliberately adjusting that. Without it hope you manually tested everything with the right assumptions.

    Why do you think interpreted makes a difference?
  • 0
    @hjk101 because with a compiler I don’t even need tests. As soon as it compiles again without errors, the refactoring is done.
  • 0
    @Lensflare here I disagree with you. There is not a single compiler that tests for correct behaviour. Just correct syntax and usage. I work with Go primarily and also with Rust these are some of the most strict compilers and still we need tests (but not unit tests nessesarily, they are overrated).

    Every interpreted language comes with the same assistance both in IDE and runtime initialization. Linters apply some further usage checks. Some interpreted languages are dynamically typed and that makes for weaker static checks. C++ while statically typed is also weak typed again limiting these checks at compile time. So I would argue that the type system and linters are what matter to you.
  • 1
    @hjk101 good point about c++. I agree.

    I also agree that not everything can be checked by a compiler and sometimes runtime tests are needed.

    However, if you write your code in a particular way, that heavily relies on the stuff that can be statically checked (if the language supports it), then in almost all cases, it is enough to guarantee correct behavior after refactoring.

    I’m doing it regularly and I don’t know when the last time was when it didn’t work out. It feels like magic and I love it.
Add Comment