Why Swift Guard Should Be Avoided
medium.com
medium.com
I usually notice functions get hard to read when they hit 80 lines or so long (generalization, of course), and try to keep mine under say 50 unless it's one of a few core functions in an application.
Interested in hearing others' experiences.
I have seen code where a function was shorter than 10 lines. ;-) In that same code base, I have seen a function that was ~750 lines. And for that particular function, it actually made sense. It involved a lot of local state, and breaking it up would have involved passing lots of parameters to the new, shorter functions. (The average function in that code base was around 50-60 lines, I guess.)
Using a fixed number, like "10 lines" seems arbitrary to me. Sometimes it makes sense, sometimes it does not. It also ignores other factors, such as comments. Or readability. Sometimes, I 100+-line function can be marvelously clear, other times, a 5-liner can be so dense it makes my eyes bleed.
I have grown wary of "Always do this"/"Never do that"-advice.
"Try to keep your functions short. If they grow long, look if it makes sense to break them up into multiple shorter functions" is sound advice. But to do so without questioning or thinking is problematic.
If I was refactoring it, and 750 lines is a massive red flag that it is in need of refactoring; then the local state indicates that it becomes not a function or a bunch of functions, but a class with state and private methods. Is that hard to spot?
So while that function was very, very long, it was relatively simple to understand and did not contain a lot of logic. Also, the fact that it was a huge switch would have made breaking it up slightly non-trivial.
Though I would still try things like :
- making each case in the switch as small as possible, preferably a 1-liner.
- making a separate object that does the "parameter mangling".
- grouping all the bits of state into an object to hold the "message loop state".
- Or most radically but best: these "map", "lookup", "strategy" style patterns that reduce it to a table of (message id, handler fn) pairs http://stackoverflow.com/a/126453
When I was hired there, that application had been in development for ~15 years, and a lot of what I did was maintenance. So making coming in as the new guy, then starting to change things, I had to tread rather carefully.
FWIW, I got each of the three developers to individually admit to me that if we had the time and the money, we would basically throw the old code out and rewrite it from scratch, using the lessons they had learnt since. But time and money being what they were, that was not likely to happen. (I was really proud I got them to admit that, though - one of the other developers was very defensive of his work, and getting him to admit that was quite an achievement; I actually gained some valuable people skills in doing so.)
I was glad, though, that the code was relatively readable and organized in a sane manner. There was some cruft that had accumulated over the years, but overall it was surprisingly sane. That was one of the first times I had to read somebody else's code, and it was good code for that purpose - sparsely commented, yet very clear.
Also, keep in mind that this was C. Different languages offer different ways of decomposing / structuring code.
But I prefer Linux take on function size:
> The maximum length of a function is inversely proportional to the complexity and indentation level of that function. So, if you have a conceptually simple function that is just one long (but simple) case-statement, where you have to do lots of small things for a lot of different cases, it's OK to have a longer function.
Also, it strongly depends on the code you are writing. Things like reading/writing data often end up as pretty long functions, while glue code between model and view is usually just a couple of lines.
Following this line of thought I've become a lot more precious about vertical whitespace & I've adopted BSD style curly braces.
But, for instance, the Python PEP8 spec saying 80 characters max, I think is kind of ridiculous these days. Who is programming on their phone, tablet, or 90s computer that can't handle 120 characters these days?
Edit: I just mention 120 as PyCharm's PEP8 linter 'erroneously' checks that max rather than 80
When trying to understand code I haven't written, I have had the experience that trying to trace logic and debug through a bunch of little functions is more cognitive overhead than just seeing everything that happens within one larger function.
I get the idea that small functions are supposed to be wonderful, and clear, if everything is named really well and everything works as the name implies.
But for me the downside is it ends up feeling like detective work. I have to jump around and remind myself exactly how each little function works just to figure out what exactly is happening.
My dream IDE would let me see "expand" every function call ... like with little dropdown arrows .. so I could see it as if it were inline, and could trace execution linearly in one solid block.
It depends on the language. In Node.js code, it's not uncommon to not only see production code that follows this rule, but also puts no more than 1 or 2 functions in each file. Once you get used to it, and as long as you're quick with your editor/IDE's open shortcuts, I've found it a pretty nice way to organize code.
I personally find C code hard to follow sometimes, not because I don't understand C (I have more experience with C than I do JavaScript, if you count years), but because C tends to feature multiple functions per file, each with hundreds of lines of code per function. At least for me, that's hard to follow, and I hate having to move hundreds of lines up and down a file, even using Vim shortcuts.
Edit: Huh? Why a downvote?
For a rule made with the goal of improving understanding, it's great that the rule is not actively detrimental to understanding.
But we still need to know how often it actually succeeds vs. having marginal effect.
We also need to know what it costs in terms of labor.
Without those two bits of info we have absolutely no idea if it was a good rule.
Extremely low benefit -- I wouldn't even notice, and most actually confusing things are bigger scale, like the confusing DAL "abstraction" that I was dragooned into copying from the wider C# community.
I've seen code where many functions were like this, and very few were over 30 lines. It was easy to read. And it was tested and generally working well. Naming becomes important, but that's part of the point of extracting functions for readability.
blabla match {
case 1 => func1
case 2 => func2
}
instead of blabla match {
case 1 =>
println("1")
1 + 2
case 2 => biggerFunc
}My gut feeling is this wouldn't pass the unofficial code reviews at my work - too many functions making the code look messy, instead of cleaner.
Does anyone have any counterpoints?
> 1. It is pretty long, sixteen lines, multiple parts separated by empty lines.
I wouldn't call 16 lines "long".
It contains empty lines for easier reading. Well done, not actually a problem.
But it lacks any comments on why the assertions have to be made.
> 2. It does several things. It gets an item by name, validates parameters, and it implements the logic of vending an item.
These are the things that naturally belong together. Separating validation from logic is not a very good idea. The logic requires the specific assertions in the validation part. They belong together.
> 3. It has several abstraction levels. The high-level vending process is hidden among the finer-level details like Boolean checks, usage of specific constants, math operations, etc.
In the single function implementation it's actually not hidden because it's all in one function. Use some comments to structure your function.
---
In my opinion separating this function into multiple functions would only make sense if these parts are used multiple times AND if they are commented well on why they need to act this way.
That's more or less the problem when discussing how to work with complex, real world software and giving examples of how to do it: the examples end up being simple enough that the techniques aren't worthwhile in that case. Don't mistake that for the techniques not being worthwhile ever.
I have seen the same when code is presented showing examples of how to use an IoC Container. The response "but this overcomplicates the code" is true, but misses that it doesn't apply to the (far larger) codebase that IoC containers are aimed at.
At first I wouldn't expect a function "validatedItemNamed" to check the price. While it checks the price it doesn't check the inventory number.
That isn't software engineering. That is religious fanaticism.
E.g.
guard item.count > 0 else {
throw VendingMachineError.OutOfStock
}
Is a very low complexity statement. One comparison is made and an enum value is thrown. You are gaining no clarity by placing this inside a separate function.The author doesn't understand the relationship, he blindly follows the advice to keep the number of lines low without considering the complexity and that leads to unnecessarily complex code.
1. If you have ever written unit test for 300 line function you know it gets dicey. Such a function will likely require too much setup and will require complex mocks/patches/doubles to be unit tested.
2. As I mentioned before, I don't believe in number of lines thingy but logical unit of separation. If a function, opens a file, reads & parses config data from it, reads URL from the config data , parses http response and writes updated bits to config data - these distinct things can be logically separated and will be easier to unit test perhaps (again production scenario might be more complicated). Also, with modern IDEs and editors jumping back&forth between functions should be a breeze and your IDE/editor should provide an outline of file which can be scanned quickly.
3. If you have written a long function that does too many logical things(not again by number of lines) - next person who makes changes to it, isn't going to break it up - even if breaking up makes sense. For example, lets say in above example original code was written with expectation of certain format of config data but now somehow the function has to support more than one config data. Now the code is so tightly coupled that - it is harder to break out parser of this new format to new function and hence the function will keep growing (and did I mention the unit test setup has to support new config format as well somehow).
3. If however, lets say method computes moving average of a timeseries, it probably makes sense to have it all in one function even if it becomes slightly longer.
TL;DR - Cargo culting must be avoided at all costs, however we don't need to throw the baby with the water. Each problem calls for our own judgement. Separate the functions by logical units, rather than number of lines, use proper editor/IDE.
Indeed it does, but if you have never even tried to go full "Uncle Bob" and write really small SRP functions and classes, then your judgement won't be well-informed. You'll be surprised how well it can work if you've never tried it. It works for readability, maintainability and testability.
Statements like the grandparent's "Lots of short functions are usually worse than a single long function" IMHO mostly show a lack of relevant experience.
Such testing is not only insufficient, but any regression that is added as a result of a change will go unnoticed too, until a real user hits that bug. You will notice how most people who are supporting long functions - don't even talk about how the heck they are going to test it.
But at the same time, just because you can transform someone's point into an ultra-generic form that's true, doesn't mean their original point has any validity. When someone gives you bathwater, don't spend too long looking for babies that aren't actually in it.
But when the article says that a function with 10 lines of code is too long and needs to be split up into a bunch of 3 line functions, that is a very extreme viewpoint. It's okay to flat-out reject such a viewpoint. It's not throwing the baby out with the bathwater.
Let's say I tell you that you need to cap your car's speed at 15mph to save gas. Don't go around telling people that "well, if you control your driving you can save gas, let's not throw the baby out with the bathwater". Just tell people I'm being dumb and to ignore me.
If the only thing true about a statement is such an extreme generalization that the generalization bears little resemblance to the original statement, just go ahead and reject the statement.
(Note: It's okay to agree with the article's view, that functions should be extremely short. This is only advice for disagreeing. I'm sorry for making my analogy intentionally ridiculous.)
guard item.count > 0 else {
throw VendingMachineError.OutOfStock
}
to if count == 0 {
throw VendingMachineError.OutOfStock
}
only proves that you can refactor the code to not use guard whereas the article starts by talking about small functions, where you could in fact still use guard in this case.Function size has been a pretty peculiar fetish. Even when viewed on the mythological 80x24 remote terminal, the initial function doesn't seem to be that hard to understand, and spreading it out between some function with lackluster naming doesn't really improve it all that much ("Computer science has two hard problems: cache invalidation, naming things and off by one errors").
Literate programming or the rather esoteric "refinements" at least had the advantage of being able to use proper sentences to describe the functional component and with the former you even had the option of being able to look at the code in both forms.
Having said that: As I'm not a Swift programmer (ba-doom-tish), is this all the language has that approaches Design By Contract?
Uncle Bob is a recognised expert in the area. So no, his opinions on SRP should not just be dismissed out of hand.
On the other hand, I do think that larger functions should be broken up into smaller parts where it makes sense, e.g. a separate function for parsing each object in the json example.
How about the function can be read top to bottom, doesn't mess with global state (unless it explicitly is documented as doing so), and has clear inputs and outputs. Who cares if it is 5,10 or 100 lines if it does that.
"Don't put multiple statements on a single line unless you have something to hide" - https://www.kernel.org/doc/Documentation/CodingStyle
(If you disagree or if you think I said something wrong and my comment deserves to be hidden, I'd love to know why!)