Comment by jjnoakes
5 years ago
I feel like this is only a problem if the small functions share a lot of global state. If each one acts upon its arguments and returns values without side effects, ordering is much less of an issue IMO.
5 years ago
I feel like this is only a problem if the small functions share a lot of global state. If each one acts upon its arguments and returns values without side effects, ordering is much less of an issue IMO.
Well, if they were one function before they probably share some state.
Clean code recommends turning that function into a class and promoting the shared state from local variables into fields. After such a "refactoring" you get a nice puzzle trying to understand what exactly happens.
I've seen threads on this before but the "goto" (couldn' t stop myself) reaching of object oriented-ness to "solve" everything is really frustrating.
I've found the single greatest contributor to more readable and maintainable code is to limit state as much as possible.
Which was really hard for me to learn because it can be somewhat less efficient, and my game programmer upbringing hates it.
Sometimes eliminating state can also mean increasing complexity and lines of code tremendously.
1 reply →
> if they were one function before they probably share some state
and this is exactly why you refactor to pull out the shared state into parameters, so that each of the "subfunctions" have zero side effects.
In javascript I sometimes break up the behaviour of a large function by putting small internal functions inside it. Those internal functions often have side effects, mutating the state of the outer function which contains them.
I find this approach a decent balance between having lots of small functions and having one big function. The result is self contained (like a function). It has the API of a function, and it can be read top to bottom. But you still get many of the readability benefits of small functions - like each of the internal methods can be named, and they’re simple and each one captures a specific thought / action.
If you're calling those functions once each in a particular order then I can't possibly figure out what that does for you that whitespace and a few comments wouldn't. How does turning 100 lines of code into 120 and shuffling it out of execution order possibly make it easier to read?
3 replies →
Aren't you creating new functions on each call to your parent function though? I imagine there must be a performance or memory penalty?
Now this is usually in my opinion not a good advice (it is like reintroduction of global variables) as unnecessary state certainly makes things more difficult to reason about.
I have read the book (not very recently) and I do not recall this but perhaps I am just immune to such advice.
I like his book about refactoring more than Clean Code but it introduced me to some good principles like SOLID (a good mnemonic), so I found it somewhat useful.
Yes and no.
What I find is that function boundaries have a bunch of hidden assumptions we don't think about.
Especially things like exceptions.
For all these utility functions are you going to check input variables, which means doing it over, over and over again. Catching exceptions everywhere etc?
A function can be used for a 'narrow use case' - but - when it's actually made available to other parts of the system, it needs to be kind of more generalized.
This is the problem.
Is it possible that 'nested functions' could provide a solution? As in, you only call the function once, in the context of some other function, so why not physically put it there?
I can have it's own stack, be tested separately if needed, but it remains exclusive to the context that it is in from a readability perspective - and you don't risk having it used for 'other things'.
You could even have an editor 'collapse' the function into a single line of code, to make the longer algorithm more readable.
The problem is abstraction isn't free. Sometimes it frees up your brain from unnecessary details and sometimes the implementation matters or the abstraction leaks.
Even something as simple as Substring which is a method we use all the time and is far more clear than most helper functions I've seen in code bases.
Is it Substring(string, index, length) or Substring(string, indexStart, indexEnd)
What happens when you pass in "abc".Substring(0,4) do you get an exception or "abc"?
What does Substring(0,-1) do? or Substring (-2,-3).
What happens when you call it on null? Sometimes this matters, sometimes it doesn't.
Also:
- Does it destructively modify the argument, or return a substring? Or both?
- If it returns a substring, is it a view over the original string, or a fresh substring that doesn't share memory with the original?
- If it returns a fresh substring, how does it do it? Is it smart or dumb about allocations? This almost never matters, except when it does.
- How does it handle multibyte characters? Do locales impact it in any way?
With the languages we have today, a big part of the function contract cannot be explicitly expressed in function signatures. And it only gets worse with more complicated tools of abstraction.
I posted this elsewhere in the thread, but local blocks that define which variables they read, mutate and export would IMO be a very good solution to this problem:
There are a couple of newer languages experimenting with concepts like this, Jai being one: https://youtu.be/5Nc68IdNKdg?t=3493
This is a fascinating idea. In some languages like C or Java or C#, the IDE can probably do this "for free" -- generate, then programmer can spot check for surprises. Or the reverse, highlight a block of code and ask the IDE to tell you about read/mutate/export. In some sense, when you use automatic refactoring tools (like IntelliJ), extract a few lines of code as a new method needs to perform similar static analysis.
In the latest IntelliJ, the IDE will visually hint about mutable, primitive-typed local variables (including method parameters). A good example is a for loop variable (i/j/k). The IDE makes it stand-out. When I write Java, I try to use final everywhere for primitive-typed local variables. (I borrowed this idea from functional programming styles.) The IDE gives me a hint if I accidentally forget to mark something as final.
> local blocks that define which variables they read, mutate and export would IMO be a very good solution to this problem:
this is basically a lambda you call instantly.
1 reply →