8 ms·
Eh. Something I find pro devs do is just code the damn thing out quickly and wait for the right abstraction to emerge before stuffing it blindly into a function
by 3pt14159 7y ago
Eh. Something I find pro devs do is just code the damn thing out quickly and wait for the right abstraction to emerge before stuffing it blindly into a function. If that means a bit of repetition, fine. If you push everything into tiny little methods or functions or abstract them into their own objects the first time you come across a couple of repeated lines of code then the clearer and better solution may not emerge as the requirements start to change. On the other hand, self documenting code is most easily done via method naming.
This type of topic is hard to talk about. It's so nuanced that saying a statement about how to do it sounds like a a gutless generality. It also depends on the programming language and lifetime of the project. I've banged out some real ugly code when servers were on fire, but it was all stuff that was destined for an early death.
- networkimprov 7y agoDocumenting code via single-caller functions is usually a mistake, because to everyone else who looks, the set of functions is an API. Both internal and external APIs must be kept coherent. Also when readers are trying to understand exactly how some function changes the system state, having to refer to numerous other functions it calls is tedious.
- OJFord 7y ago> Documenting code via single-caller functions is usually a mistake, because to everyone else who looks, the set of functions is an API. That's a really good way of putting it. I try to avoid it by not writing new functions, but gladly using existing ones, in my implementation of whatever single new one. For example if I'm writing a find_and_update_foobar function, I'll use find_foobar if it exists, and with the right signature, but I won't write it just to implement the one I actually care about; ditto update_foobar. But, professionally I've mainly only used python; so I find it still deteriorates into a mess. (I type hint extensively, but still it only takes some missing hints, or something too loosely - or wrongly - typed.) I haven't used rust professionally/enough/on something large enough to be sure, but my feeling is that it just having a type checker prevents so much mis-refactoring.
- nicoburns 7y agoInterestingly, Rust can be quite opinionated abour code organisation (it makes you keep things in bigger chunks), because some of the compiler analysis doesn't work through function boundaries.
- heavenlyblue 7y agoI think this one is bullshit, can anyone else comment on this one? Are there things you may only do iff you using a single function body?
- nicoburns 7y agoA key one is partial borrows of structs (i.e. borrowing only 1 field, and then later borrowing a different one).
- zelphirkalt 7y agoBut then what happens to "Your function should be doing one thing and one thing only." (The thing indicated by its name, considering the current abstraction level.)? Perhaps it is not always a bad idea to decompose that function into 2 properly named "steps", which reside in their own functions.
- cma 7y ago> Also when readers are trying to understand exactly how some function changes the system state, having to refer to numerous other functions it calls is tedious. This also applies for small inlined common helper functions. If it folds two or three operations into one, but doesn't have an obvious universally recognizable name, it would often be way clearer to a reader of the code to just put the few operations in directly.
- marcosdumay 7y ago> Also when readers are trying to understand exactly how some function changes the system state Yep. That's why changing system state is better kept at a minimum, and at the highest layer possible. Most languages have a very clear public/private marker for determining what is an API and what is just there for convenience (it doesn't have to be literally public/private, for some it's scope, others just tag the names, etc). You don't need to change a code's style just because of it. On the other hand, if the functions do not have logical and atomic meanings, they will do make your code as hard to understand as it will be to name them.
- notJim 7y agoThis is such a good, succinct way of putting it. One of the worst code-bases I've ever had to work with was largely so because of so many single-caller functions and far too granular abstraction.
- threatofrain 7y agoIt seems that's how a few major libraries are organized, sometimes one function per file.
- emsy 7y agoSince this comment finds a lot of agreement I'm going to play the devils advocate. Private function for the sake of documentation are fine. At least if they are all on the same level of abstraction. If you've ever worked in a team where everyone works this way you stop thinking in APIs, which can be a good thing for internal code.
- BurningFrog 7y ago> Documenting code via single-caller functions is usually a mistake, because to everyone else who looks, the set of functions is an API. Not when you work on a team that does this as a practice. Many teams don't manage to have a common code style, which is a big problem in itself. > Also when readers are trying to understand exactly how some function changes the system state, having to refer to numerous other functions it calls is tedious. The point of breaking out the small functions is to name them so understanding the code becomes easier. This takes some skill and thought, of course, and if done thoughtlessly, it will not be good. But that's true of every practice...
- barrkel 7y agoIt especially doesn't work in a team, because under maintenance + employee turnover, these little single-purpose functions start growing hairs, and they end up doing more than they're advertised to by name. Next thing you know, you're fetching the same thing from the database in three different functions, or recalculating the same value repeatedly. Overly granular functions constructed to name blocks of code are a firm anti-pattern in my book.
- emsy 7y agoFunctions overbearing their purpose can be done in large functions too, arguably even easier because it’s harder to express the precise purpose in the function‘s name. And the turnover argument applies to any code convention, which makes it a management issue.
- BurningFrog 7y agoSounds like "team" to you means "dysfunction". I'm sorry you've been in these teams. There are good teams out there!
- barrkel 7y agoOver a multi-year or multi-decade time horizon, it just deteriorates. The oldest codebases I've worked on were 30 years old; the one at my current company is about 10 years old. People come and go, refactorings are started and stopped, bugs are fixed on a tight schedule, and the structure of code divided into nominal blocks slowly dissolves.
- hinkley 7y agoSpreading state mutation out across the system is almost always a bad idea. I would categorize that person as doing DRY badly. Coalescing state transitions should trump decomposition. But it’s often the case that you can do both at once.
- Marazan 7y agoThat second paragraph should be on the front page of the Redux website.
- acemarke 7y agoNoted! https://github.com/reduxjs/redux/issues/3592#issuecomment-586620584 https://github.com/reduxjs/redux/issues/3592#issuecomment-58... :)
- FeepingCreature 7y agoThis is wise. Always try to keep your state change points concentrated and legible.
- stormdennis 7y agoCan anyone ELI5 the first sentence in each of the two above paragraphs please?
- hinkley 7y agoI can ELI20 easily enough. 5 is a bit tougher. Don’t make me look under every rock to figure out why you changed the stuff I gave you. We aren’t built to deal with chaos. When you put stuff in one end and something random comes out the other end, you have no idea at all what’s going on (until you do, and then they try to put you in charge). You want heaps of code that looks but doesn’t touch. Push the changes to the edges where they are easy to see. If I can trust that things in the middle aren’t mucking around with things all the time then lots of little methods don’t hurt my ability to think about what is happening. (This is not quite the point of Hexagonal Architecture, but they have some common ground. See also early Angular’s philosophy of cooking all input data immediately and passing it around cooked.)
- pvorb 7y agoIdeally the function's name reveals what it's doing. Only if you are tracing down a problem you are required to look into the details. This actually helps to speed up navigating code, because parts of it get meaningful names.
- networkimprov 7y ago"Only if you are tracing down a problem you are required to look into the details." This is the common justification, and it's misguided. It turns out that you need to be aware of the "details" every time you look at the code. If, by looking at the code enough, you memorize the "details," it's tempting to move them to a new function with a clever name that tickles your memory. That won't help anyone else. Use comments to introduce blocks of code that need explaining. Use functions in coherent APIs.
- dragonwriter 7y ago> This is the common justification, and it's misguided. It turns out that you need to be aware of the "details" every time you look at the code. No, I don't. I need to be aware of the relevant details, but code in functions with meaningful names mean (1) I can skim a high level overview to see where the details relative to my current interest we likely to be faster and, (2).I can zoom in without distraction to those more easily. > Use comments to introduce blocks of code that need explaining. Comments make a wall of code that is already hard to get an overview of because of its size less legible, decomposition did the opposite.
- nicoburns 7y agoDisagree. Comments can add "section headers", while keeping the code flow linear, so I can skim it just like I skim an article. With methods, I have to jump backwards and forwards and have to remember my place as well as think about the code. This introduces unnecessary overhead.
- christophilus 7y ago
- dragonwriter 7y ago> Documenting code via single-caller functions is usually a mistake, because to everyone else who looks, the set of functions is an API. That's less the case if the functions are local (either not exported from the module they are included in or actually function-local, in languages supporting nested functions, to their caller. > Both internal and external APIs must be kept coherent. If it's not external, it's not an application programming interface. And, even ignoring that, there is no definition of coherent for which that approximates truth that is inconsistent with single use functions for code clarity and organization. Even languages that don't have mechanisms to make methods/functions truly local/private tend to have conventions for indicating functions/methods not part of the intended-as-public API of a class or module. > Also when readers are trying to understand exactly how some function changes the system state, having to refer to numerous other functions it calls is tedious. Conversely, I find the code having functions with descriptive names helps me not have to read through a bunch of irrelevant code when I'm doing that, which helps me get to the part I need to understand whatever I'm trying to understand faster and with less distraction. Yes, if it's done badly it's problematic, but that's true of literally everything. EDIT: > Also when readers are trying to understand exactly how some function changes the system state Yes, if you are doing relatively unconstrained imperative code that's willy-nilly modifying state, breaking that up into functions passing mutable state around is likely to make it even more incomprehensible. Decomposing into units that are externally-pure functions (that is, while they may do local mutations, they don't modify any state received from the caller), though, is not problematic. In general, except for subsystems dedicated to managing mutable state (and where this function is kept as constrained as possible), I prefer coding in externally-pure functions in general. When you start with that and use it as a constraint on decomposition, decomposition can no longer serve to obscure state modifications. Indeed, it clarifies and more narrowly isolates them.
- networkimprov 7y ago> If it's not external, it's not an application programming interface Splitting hairs, I think. Even if not intended, someone else may assume your "documentation" function was meant to be generally accessible (i.e. an API), and call it that way.
- nnq 7y agoI found some simple rules for single caller functions: 1. Use them if they take <5 parametets - no point making a function if you need to pass all the local context into it 2. Use them iff they return something that can be clearly named and have an explainable and understandable TYPE, even in dynamic langs where you don't explictly type things - if you'd end up returning a tuple of 3 arbitrary things that can't be "named as one single concept" it's a code smell 3. Kepp those functions local, maybe define them inside the calling function, at least private if they're methods, anything - if your single caller function becomes unwillingly part of an api, which can esily happen in Python with a _funk() method that users could ignore it's private, you've lost
- benibela 7y agoPascal has the solution for that. Nested functions: procedure foobar; procedure helper1; begin end; procedure helper2; procedure subhelper2; begin end; begin end; procedure helper3; begin end; begin end; Only the foobar function can access the helper functions.
- dragonwriter 7y agoPascal is hardly unique in that; many languages have nested functions, and every language that has first class functions and local variables/constants has the equivalent of nested functions (potentially with even narrower scope than a whole function) even if it doesn't have a specialty nested function declaration syntax.
- networkimprov 7y agoSince nested functions don't create new API, amen.
- loopz 7y agoWhat may help is pondering about object encapsulation, composition and usage patterns. Ie. either be the domain expert, or develop into one by building the domain. In order to do this, recognize iterations are key and that code must both work today and allow for future evolution. With clearer goals, one might save time and effort refraining from premature optimization and overzealous refactoring. When direction is unclear, you can comfortably lean on established accomplishments while avoiding regressions. DRY and YAGNI may thus take a backseat to incrementally deductive design.
- deleted 7y ago[deleted]
- crimsonalucard 7y agoFunctional programming via composition solves this problem by making your functions composable and easily decomposable. The right abstraction under this methodology is mostly correct every time and if not the abstraction is easily broken apart and recomposed with a modification due to the fact that functions can be decomposed.
- papito 7y agoThe rule of threes. It is VERY hard to nail an abstraction until you see slightly different implementations of it at least three times. Also, I think we kind of said goodbye to DRY after we decided that microservices is The Way. How many microservices reinvent the same goddamn wheel?
- jknoepfler 7y agoWhich is why we template, build dsls and generic support libraries, and generally automate the hell out of microservice boilerplate. (Usually after a lot of slogging).
- papito 7y agoSlogging. Exactly. The up front effort required to get microservices right is absolutely massive, but somehow it has become the "easy" way to solve all your problems. Distributed systems were and should be a problem to be avoided at all costs, and only then tackled by those few who know what they are doing.
- hinkley 7y agoI have been experimenting with code reuse via repository forking. Not much to show for it so far, unfortunately.
- tylerjwilk00 7y agoSo true. I'd almost consider it a kind of premature optimization. Trying to nail down DRY abstractions not too early but not too late is often tricky but concern over it probably puts you in good company.
- pvorb 7y agoOne problem with code that is destined for an early death: It usually lives (much) longer than originally expected.
- iovrthoughtthis 7y agoI think this is a human reaction to their environment. 1. Write code 2. Cleanup the mess 3. Write more code 4. Realise there a better way. 5. Stick with it because effort to change < benefit gained. In Refactoring Kent Beck doesn’t just show extracting variables, functions and classes but also re-inlining them. The problem is at scale, re-inlining is hard. If it were easier to do, it might be done more. I find that waiting for N repetitions of some abstractable code is a heuristic to mediate between the cost and benefit of refactoring. I don’t have an answer for improving it. Perhaps making easier to automatically manipulate the code in this manner? I’ve thought for a while that it would be interesting to treat a code base as a database. Applying documented migrations to it which describe the code transformations occurring (and how to reverse them). Then changing these aspects of code would be trivial. But then your meta program, the set of transformations which generate your current code base, would probably suffer the same problems as your codebase.
- tigershark 7y agoI instead find that when using functional languages the code begs you to go in a small, tiny function. It makes the intent much more clear about what that code is doing and 99% of the time you don’t need to know how is doing it. Obviously if that code is used only once then it doesn’t make sense to extract it, but I find out that small pure functions are really composable, powerful and reusable. Maybe the best example is the pipe operator definition in F#: let (|>) x f = f x It’s extremely simple, extremely powerful and it’s used pretty much everywhere.
- thdrdt 7y agoSome things overlap, but I think your comment is more about KISS. Early abstractions don't fit KISS. And I believe banging out ugly code can still fit KISS because it does the job. DRY falls into a different category. For example if you use `.toFixed(2)` to format floats everywhere it is better to abstract it to a function. And I believe this is what the article is about. Formatting a float is a kind of logic or knowledge about how you present values in the view. You should not repeat this knowledge because if it is later decided that the view must show 3 decimals you are in trouble. Maybe DRY can be easily explained by: don't repeat (business) logic.
- golergka 7y agoThat's a good strategy – but you have to be confident enough that when the right abstraction emerges, you will be able to squeeze necessary refactoring in between of other tasks that need to be done. It's ready to do when you're a team lead or a solo developer, but when someone else has technical leadership, you won't be able to do this without their support.