8 ms·
It’s a matter of style, and like cooking, either too much or too little salt will ruin a dish. In this case I hope nobody is proposing a single 1000-line god f
by theptip 3y ago
It’s a matter of style, and like cooking, either too much or too little salt will ruin a dish.
In this case I hope nobody is proposing a single 1000-line god function. Nor is a maximum of 5 lines per function going to read well. So where do we split things?
This requires judgment, and yes, good taste. Also iteration. Just because the first place you tried to carve an abstraction didn’t work well, doesn’t mean you give up on abstractions; after refactoring a few times you’ll get an API that makes sense, hopefully with classes that match the business domain clearly.
But at the same time, don’t be over-eager to abstract, or mortally offended by a few lines of duplication. Premature abstraction often ends up coupling code that should not have to evolve together.
As a stylistic device, extracting a function which will only be called in one place to abstract away a unit of work can really clean up an algorithm; especially if you can hide boilerplate or prevent mixing of infra and domain concerns like business logic and DB connection handling. But again I’d recommend using this judiciously, and avoiding breaking up steps that should really be at the same level of abstraction.
- atoav 3y agoIf we go with the cooking analogy, if you have to describe to someone how to cook a meal, and at one part of the meal you have to put the fond in, it is reasonable to explain how to make the fond in a seperate section. The fond is it's own thing and it has one touching point with the food,therefore it is okay (or even benefitial) to move it out. Also: cooking recipes are also very abstracted. When they say you need to lightly fry onions they assume you know a way to cut onions and a lightly frying algorithm already. If they would inline everything it would become unreadable. Code is very similar. If you want it strictly without abstractions it will be as low level as your language allows you, and that is definitely not readable code. If you e.g. instead of using pythons "decode" method tries to do unicode decoding yourself it would become very hard to understand what your program is actually about. Now there are probably zero people who would do that, because the language provides a simple and well tested abstraction — but what makes that different from you creating your own simple and well tested abstraction and using that throughout the actual business logic of your code? The hard part is creating abstractions that are so well chosen that nobody will have to ever touch them again.
- yccs27 3y agoTo stay with the fond analogy: It gets interesting if the fond preparation involves deglazing a pan (mutable environment) with meat bits and juices left at the bottom (state/precondition). Two options: - Linear code: The meat frying (state-producing) and deglazing (state-requiring) steps are below each other in the same recipe, so to verify that it works you can just linearly go through line by line. However if the recipe becomes long and a lot of stuff happens in between, it's no longer obvious. You'll have to use good comments ("// leave residue in the pan, we'll need it for the fond") because otherwise you might accidentally refactor in a way that violates the precondition (swaps/scrubs the pan). - Modular code: You need to clearly describe the precondition on the fond preparation subroutine to have any chance to keep using it correctly. On one hand this forces documentation, on the other hand it's probably still easier to forget since the subroutine call ("Prepare the fond.") doesn't directly make the precondition obvious. Either way has its advantages and drawbacks, and the right choice depends on the circumstances. This is assuming you only want to cook this specific meal and aren't writing a cookbook - otherwise you should definitely modularize to remove repetition.
- ncann 3y ago> In this case I hope nobody is proposing a single 1000-line god function. Nor is a maximum of 5 lines per function going to read well. This is the key. Novice devs tend to write giant functions. Zealot devs who read books like Clean Code for the first time tend to split things to a million functions, each one a few lines long (pretty sure the book itself says no more than 5 lines for each function). I worked with a guy who extracted each and every boolean condition to a function because "it's easier to read", while never writing any comments because "comments are bad" (according to the book). I hate that book, it creates these zealots that mindlessly follow its bad advices.
- livrem 3y agoClean Code says "Functions should not be 100 lines long. Functions should hardly ever be 20 lines long". I think both 100 and 20 are a bit low, but much better than 5. As I mentioned in a comment a few days ago when I also corrected someone that misremembered a detail from the book, I am not a huge fan. But I also think it is mostly correct about most things, and not as terribly bad as some say. Listening to fans of the book is more annoying than to actually read the book. (And that other comment when I corrected someone was about bad comments. Clean Code definitely does not say that you shall never comment anything.)
- raincole 3y agoIt's not from Clean Code, but Refactoring.
- ahoka 3y agoA classic criticism of CC: https://qntm.org/clean https://qntm.org/clean
- maleldil 3y agoI love qntm's writing, be it criticism a technical book or blowing my mind with science fiction. I strongly recommend "There Is No Antimemetics Division".
- 3y ago
- wiseowise 3y ago> In this case I hope nobody is proposing a single 1000-line god function. I’ll take well-structured 1000-lines function over bad spaghetti of hundreds small functions any day.
- leoedin 3y agoHave you ever seen a well structured 1000 line function? I'm sure they exist - maybe some sort of exceedingly complicated data transform or something. But in almost every situation I've seen, a 1000 line function has countless side effects, probably sets a few globals, takes loads of poorly named arguments, each of which is a nested data structure which it reaches deeply into and often has the same for loop copied and pasted 10 times with one character changed. Often a 1000 line function is actually 5 or 6 20 line functions. I'm sure there are legitimate exceptions, but I've never seen them.
- deelly 3y agoI`m not OP, but yes, I saw. Thats my personal opinion, and nothing more: Something like complex one time financial/workflow/maintenance operation that includes calling dozens of different smaller functions, but very well structured. It does not make sense to separate it more into different functions, because execution is generally linear and having to deal with tree of calls where some calls is depends on state of the previous is become cumbersome and makes reading and making changes more complex. Again, thats my personal feeling, and nothing more.
- deleted 3y ago[deleted]
- Thorrez 3y agohttps://github.com/github/putty/blob/master/terminal.c#L3281 https://github.com/github/putty/blob/master/terminal.c#L3281 This function is 1830 lines long. It's reasonably well structured I think. Although the #if 0 are maybe not so good.
- dsego 3y ago> Also iteration. Just because the first place you tried to carve an abstraction didn’t work well, doesn’t mean you give up on abstractions; C. Muratori calls this method "semantic compression" . https://caseymuratori.com/blog_0015 https://caseymuratori.com/blog_0015
- djur 3y agoWhat's described there is what I understand DRY ("don't repeat yourself") and the associated "rule of three" to mean.
- coldtea 3y ago>In this case I hope nobody is proposing a single 1000-line god function. Why not? Who said it's worse? What study settles the issue? Some times a "1000-line god function" is just what the domain needs, and can be way more readable, with the logic and operations consolidated, than 20 50 line functions, that you still have to read to understand the whole thing (and which then someone will be tempted to reuse a few, adjust them for 2-3 different needs not had by your original operation, and tie parts of the functions implementing your specific logic to irrelevant to it use cases). And if it's a pure 1000-line function, it could even be 10,000 lines for all I care, and it would still be fine.
- visarga 3y agoYeah, when code gets spread out across too many classes and functions, it's like you're trying to navigate a maze without a map. You hit a breakpoint, and you're left scratching your head, trying to figure out what the heck each class is supposed to do. Names can be deceptive, and before you know it, the whole architecture feels like a jigsaw puzzle. It's a cognitive load, having to keep track of all these quirks. Maybe it was easier for the author to do it that way when they started from scratch, but after they finished, it's another deal.
- orwin 3y agoI think the only good use case I have for 50+ lines functions are finite state machines and renderers, whatever the form. Do you have other examples of 50+ lines functions where you thought it was the best to not separate issues?
- fingerlocks 3y agoSpecialized parsers. Encoding and decoding tasks. Complex computation is often isolated to help with peer review. Pattern matching routines. Also, constructors with many validation steps that are compiler constrained to their local scope. That seems common.
- orwin 3y agoAren't specialized lexer/parsers finite state machines? As are encoder/decoders? OK fir complex computation, I left the world of mathematics 7 years ago, and I wasn't at the edge on that, I trust you, but to be clear, all your examples scream 'FSM' to me. If you have a pattern matching routine of 50+ lines that isn't a finite state machine, you're doing something wrong imho, and should consider changing abstraction (I'm not a big OO guy, but maybe use dynamic dispatch?)
- ed_blackburn 3y agoAs soon as conversations stray into lines of code etc. I think we've veered directly into Goodhart's Law.
- wouldbecouldbe 3y agoMy biggest pain is Javascript developers who get to high on Java concepts, most often after using NestJS. Providers, Models, Services and what not. Remember an import script I wrote in ExpressJs. Was like 50 lines. Did things like copy databases, clean up config etc. There were hardly any layered ifs, just steps, I didn't see much use in breaking it up, was easy to read. Another developer, who was smart but liked abstract concepts, overenginered the hell out of it, moving it to 20 places, a bunch of provider, and I could never find & make sense out of it after that, was very hard to read was going on. Was such a pain always to update it.
- DanielHB 3y agoThe main reason I have a distaste for dependency injection is because of this, promotes separating code into multiple places and over-abstracting things, making code hard to follow. Most of the times it is not worth the trade-off. Doing module mocking for unit tests instead of dependency injection in runtime code is almost always a better idea in my opinion. Dependency injection was invented for languages that can't do module mocking.
- nucleardog 3y ago> But at the same time, don’t be over-eager to abstract, or mortally offended by a few lines of duplication. Premature abstraction often ends up coupling code that should not have to evolve together. A relatively common piece of feedback from me to the team at work is usually to take a half step back and look at the larger problem domain and consider whether these things are necessarily the same, or coincidentally the same. Just because the lines of code look similar right now doesn't mean they need to be that way or need to stay that way. Trying to mash together two disparate use cases because "the code's basically repeated" is often how you get abstractions that, especially over time, end up not actually abstracting anything. As the various use cases get too divergent, the implementations either move much of the logic up to the caller (shallow abstractions, little value), or expose the differences via flags and end up with two very different implementations under the hood side-by-side (less clear than two independent implementations).
- naikrovek 3y ago> In this case I hope nobody is proposing a single 1000-line god function. this made me feel a certain type of way. (dont ever look at video game source code, by the way; 1000-lines is quite short by some standards) if a 1000-line long main is what makes sense then you should do that. I find 1000-line long methods which are linear far easier to read than code which has every method call broken out into its own method. it's so bad I literally can't read JavaScript that is written in the contemporary style anymore. absolutely impenetrable for me. it's true that I am not a "real" developer in that I don't work on code full-time, but I've written probably millions of lines of code in my 30-year career. I am not a novice. if the solution calls for a 1000-line main method, then that's what I'm writing, "best practices" can go in the corner and cry. I'm writing what I need to solve the problem and nothing more.
- westurner 3y ago> So where do we split things? Cyclomatic complexity: https://en.wikipedia.org/wiki/Cyclomatic_complexity https://en.wikipedia.org/wiki/Cyclomatic_complexity Overhead: https://en.wikipedia.org/wiki/Overhead_(computing) https://en.wikipedia.org/wiki/Overhead_(computing) Some programming language implementations and operating systems have more overhead for function calls, green threads, threads, and processes. If each function call creates a new scope, and it's not a stackless language implementation, there's probably a hashmap/dict/object for each function call unless TCO Tail-Call Optimization has occurred. Though, function call overhead may be less important than Readability and Maintainability The compiler or interpreter can in some cases minimize e.g. function call overhead with a second pass or "peephole optimization". Peephole optimization: https://em.wikipedia.org/wiki/Peephole_optimization https://em.wikipedia.org/wiki/Peephole_optimization Code linting tools measure [McCabe,] Cyclomatic Complexity but not Algorithmic Complexity (or the overhead of O(1) lookup after data structure initialization).
- xorcist 3y agoIt is easy to nod along when someone speaks about different styles. But there are also a few objective truths down there, and it makes sense to try to identify them. For example, I have been at this for over three decades now, and there are some things that almost never fails. From the article, the kind of person who advocates for the more "testable" code with a few more lines and more abstractions, is never the same person who can maintain that codebase a handful years later. That should tell us something. For what it's worth, I agree with the article that simpler is better, which often coincides with fewer lines of code. I personally wouldn't have chosen objects that look like "pizza.Sliced = box.SlicePizza()" but most of the time the structure is already in place and it is best to go along with it. As to that 1000 line function, if it is in an imperative style it might well be the easiest form to read. Have you seen the Python source code? That language success owes to a simple interpreter with ginormous functions that anyone and their brother can read from top to bottom and dare modify without having the brain the size of a planet.
- pvillano 3y agoSometimes I use an anonymous scope instead of extracting a single use function. This is especially nice when you would otherwise have many parameters/returns