[HN Gopher] Good Software Development Habits
___________________________________________________________________
Good Software Development Habits
Author : mmphosis
Score : 208 points
Date : 2024-11-17 16:34 UTC (6 hours ago)
(HTM) web link (zarar.dev)
(TXT) w3m dump (zarar.dev)
| leetrout wrote:
| > It's better to have some wonky parameterization than it is to
| have multiple implementations of nearly the same thing. Improving
| the parameters will be easier than to consolidate four different
| implementations if this situation comes up again.
|
| Hard disagree. If you cant decompose to avoid "wonky parameters"
| then keep them separate. Big smell is boolean flags (avoid
| altogether when you can) and more than one enum parameter.
|
| IME "heavy" function signatures are always making things harder
| to maintain.
| thfuran wrote:
| I think it's especially bad advice with the "copy paste once is
| okay". You absolutely do not want multiple (even just two)
| copies of what's meant to be exactly the same functionality,
| since now they can accidentally evolve separately. But coupling
| together things that only happen to be mostly similar even at
| the expense of complicating their implementation and interface
| just makes things harder to reason about and work with.
| atoav wrote:
| My experience is totally different. Sure the popular
| beginners advice is to never repeat yourself, but in many
| cases that can actually be a viable operation, especially
| when you are okay with functions drifting apart or the cases
| they handle are allowed to differ.
|
| And that happens.
|
| The beginners problem lies in the reasons why that happens --
| e.g. very often the reason is that someone didn't really
| think about their argument and return data types, how
| functions access needed context data, how to return when
| functions can error in multiple ways etc, so if you find
| yourself reimplementing the same thing twice because of that
| -- sure thing, you shouldn't -- what you should do is go back
| and think better about how data is supposed to flow.
|
| But if you have a data flow that you are very confident with
| and you need to do two things that just differ slightly just
| copy and paste it into two distinct functions, as this is
| what you want to have in some cases.
|
| Dogmatism gets you only so far in programming.
| wruza wrote:
| I think that it's our tooling sucks, not us. Cause we only
| have functions and duplicated code, but there's no named-
| common-block idea, which one could insert, edit and
|
| 1) see how it differs from the original immediately next
| time
|
| 2) other devs would see that it's not _just_ code, but a
| part of a common block, and follow ideas from it
|
| 3) changes to the original block would be merge-compatible
| downwards (and actually pending)
|
| 4) can eject code from this hierarchy in case it completely
| diverges and cannot be maintained as a part of it anymore
|
| Instead we generate this thread over and over again but no
| one can define "good {structure,design,circumstances}" etc.
| It's all at the "feeling" level and doing so or so in the
| clueless beginning makes it hard to change later.
| dllthomas wrote:
| I think a part of the problem is that in addition to being
| a well regarded principle with a good pedigree, "DRY" is
| both catchy and (unlike SOLID or similar) seems self
| explanatory. The natural interpretation, however, doesn't
| really match what was written in The Pragmatic Programmer,
| where it doesn't speak of duplicate code but rather
| duplicate "pieces of information". If "you are okay with
| functions drifting apart or the cases they handle are
| allowed to differ" then the two functions really don't
| represent the same piece of information, and collapsing
| them may be better or worse but it is no more DRY by that
| definition.
|
| I've tried to counter-meme with the joke that collapsing
| superficially similar code isn't improving it, but
| compressing it, and that we should refer to such activity
| as "Huffman coding".
|
| It's also worth noting that the focus on syntax can also
| miss cases where DRY would recommend a change; if you are
| saying "there is a button here" in HTML and also in CSS and
| also in JS, your code isn't DRY even if those three look
| nothing alike (though whether the steps necessary to
| collapse those will very much depend on context).
| jajko wrote:
| The problem is, such decisions are taken in the beginning of
| the project when you are far from full picture. Then comes
| rest of the app lifecycle - decade(s) of changes, bugfixes,
| replatformings, data/os/cluster migrations and so on.
|
| I've seen, and even currently work on stuff that has
| beautiful but hard-to-grok abstractions all over the place
| (typical result of work of unsupervised brilliant juniors,
| technical debt in gigatons down the line but its almost
| always other people's problem). The thing is, that code has
| seen 10 major projects, absorbed other stuff, meaning and
| structure of data changed few times, other systems kept
| evolving etc.
|
| Now all those abstractions are proper hell to navigate and
| perform any meaningful change. Of course another typical
| brilliant 5-second-attention-span junior result is complete
| lack of documentation. So you see stuff happening, but no
| idea why or why not, what does it mean down the line in other
| systems, why such choices were made and so on.
|
| These days, I've had enough of any-design-patterns-at-all-
| costs kool aid and over-engineered cathedrals for rather
| trivial stuff (I think its mostly down to the anxious ego
| issue but thats for another discussion), I am more than happy
| to copy&paste stuff even 20x - if it makes sense at that
| place. And it does surprisingly often. Yes its very uncool
| and I won't brag about it on my next job interview, but it
| keeps things refreshingly and boringly stable and
| surprisingly also easier to change and test consequences, and
| somehow that's the priority #1 for most of the companies.
| ninkendo wrote:
| Every time you consider copy pasting, you should be asking
| yourself "if the stuff I'm pasting needs to change, will I
| want both of these places to change?" It requires some
| guessing the future, but usually it's not hard to answer the
| question.
|
| IME if something should be an independent function or module,
| I rarely get to the point of considering copy/pasting it in
| the first place. If I want to copy/paste it's usually because
| the two places currently only _incidentally_ need the same
| code _now_ , and my gut usually tells me that it will no
| longer be the case if I have to make any sort of change.
| mewpmewp2 wrote:
| Early in my career I started out really DRY, it in my
| experience and not just the code I wrote led to various
| issues down the line with unmaintainable edge cases.
| Especially if many teams are working on those things. It
| becomes really hard to support at some point. Now I feel
| much better making things DRY when it is really obvious
| that it should be.
| dllthomas wrote:
| > I started out really DRY
|
| When you say "DRY" here, would you say you had
| familiarity with the original definition, or merely what
| you (quite understandably) inferred from the acronym?
| Because I think the formulation in The Pragmatic
| Programmer is pretty spot on in speaking about not
| repeating "pieces of information", whereas I find in
| practice most people are reacting to superficial
| similarity (which may or may not reflect a deeper
| connection).
| hinkley wrote:
| And usually the answer stops becoming a guess at 3. I've
| certainly had enough experiences where we had 2 and 3 in
| the backlog and no matter how we tried, #3 always required
| as much or more work than #2 because we guessed wrong and
| it would have been faster to slam out #2 and let #3 be the
| expensive one.
| charles_f wrote:
| That's not entirely true. The difference between intentional
| and accidental repetition is that the first occurs because
| the rule is the same in both repetitions, and should be the
| same ; whereas the second happens to be the same _for now_.
| In not repeating yourself in the second case you actually
| risk changing an operation that should remain the same, as a
| side effect of changing the common function to alter the
| behaviour of the first.
|
| That's why DRY is a smell (indicates that something might be
| wrong) and not a rule.
| chipdart wrote:
| > I think it's especially bad advice with the "copy paste
| once is okay". You absolutely do not want multiple (even just
| two) copies of what's meant to be exactly the same
| functionality, since now they can accidentally evolve
| separately.
|
| Hard disagree. Your type of misconception is the root cause
| of most broken and unmaintainable projects, and the root of
| most technical debt and accidental complexity.
|
| People who follow that simplistic logic of "code can
| accidentally evolve separately" are completely oblivious to
| the fact that there is seemingly duplicate code which is only
| incidentally duplicate, but at its core should clearly be and
| remain completely decoupled.
|
| More to the point, refactoring two member functions that are
| mostly the same is far simpler than refactoring N classes and
| interfaces registered in dependency injection systems
| required to DRY up code.
|
| I lost count I had to stop shortsighted junior developers who
| completely lost track of what they were doing and with a
| straight face were citing DRY to justify adding three classes
| and a interface to implement a strategy pattern because by
| that they would avoid adding a duplicate method. Absurd.
|
| People would far better if instead of mindlessly parrot DRY
| they looked at what they are doing and understood that
| premature abstractions cause far more problems than the ones
| they solve (if any).
|
| Newbie, inexperienced developers write complex code.
| Experienced, seasoned developers write simple code. Knowing
| the importance of having duplicate code is a key factor.
| l33t7332273 wrote:
| > Newbie, inexperienced developers write complex code.
| Experienced, seasoned developers write simple code
|
| This is a really inaccurate generalization. Maybe you could
| say something about excess complexity, but all problems
| have some level of irreducible complexity that code
| fundamentally had to reflect.
| necovek wrote:
| Nope, it is not inaccurate -- but you are not wrong
| either.
|
| Obviously, code will reflect the complexity of the
| problem.
|
| But incidentally, most problems we solve with code are
| not that hard, yet most code is extremely complex -- a
| lot more complex than the complexity inherent to the
| problem. And that's where you can tell an experienced,
| seasoned (and smart) developer who'd write code that's
| only complex where it needs to be, from an inexperienced
| one where code will be complex so it appears "smart".
| ChrisMarshallNY wrote:
| Don't look at the code I just wrote (populating a user
| list with avatars, downloaded via background threads). It
| might cause trauma.
|
| The last couple of days have been annoying, but I got it
| to work; just not as easily as I wanted. The platform,
| itself, has limitations, and I needed to find these, by
| banging into them, and coding around them, which is ugly.
| stouset wrote:
| All walks of developers write overly-complex code because
| they don't know _how_ to abstract so they either overdo it,
| under-do it, or just do it badly.
|
| Writing good abstractions is hard and takes practice.
| Unfortunately the current zeitgeist has (IMO) swung too
| hard the wrong way with guiding mantras like "explicitness"
| which is misinterpreted to mean inline all the logic and
| expose all the details everywhere all the time and "worse
| is better" which is misinterpreted to justify straight up
| bad designs / implementations in the name of not
| overthinking things, instead of good-but-imperfect ones.
|
| The knee-jerk response against abstraction has led to the
| majority of even seasoned, experienced developers to write
| overly complex code because they've spent a career failing
| to learn how to abstract. I'd rather us as an industry
| figure out what makes a quality abstraction and give
| guidance to junior developers so they learn how to do so
| responsibly instead of throwing up our hands and acting
| like it's impossible. This despite literally all of
| computing having been built upon a tower of countless
| abstractions that let us conveniently forget the fact that
| we're actually juggling electrons around on rocks.
| twic wrote:
| What thfuran said was:
|
| > You absolutely do not want multiple (even just two)
| copies of what's meant to be exactly the same
| functionality, since now they can accidentally evolve
| separately. But coupling together things that only happen
| to be mostly similar even at the expense of complicating
| their implementation and interface just makes things harder
| to reason about and work with.
|
| So, if things are fundamentally the same, do not duplicate,
| but if they are fundamentally different, do not unify. This
| is absolutely correct.
|
| To which you replied:
|
| > People who follow that simplistic logic of "code can
| accidentally evolve separately" are completely oblivious to
| the fact that there is seemingly duplicate code which is
| only incidentally duplicate, but at its core should clearly
| be and remain completely decoupled.
|
| Despite the fact that _this is exactly what the comment you
| replied to says_.
|
| Then you go on a clearly very deeply felt rant about
| overcomplication via dependency injection and architecture
| astronautics and so on. Preach it! But this is also nothing
| to do with what thfuran wrote.
|
| > Newbie, inexperienced developers write complex code.
| Experienced, seasoned developers write simple code.
|
| Sounds like the kind of overgeneralisation that
| overconfident mid-career developers make to me.
| deely3 wrote:
| The issue is that you actually never really know is
| things are fundamentally the same. To know it you have to
| know the future.
| ikrenji wrote:
| DRY fanaticism is just as bad as not thinking about DRY at
| all
| gwbas1c wrote:
| In those situations, you really have multiple functions
| intertwined into a single function. Refactor to give each
| caller its own version of the function, and then refactor so
| that there isn't copy & paste with the similarities.
| bloopernova wrote:
| Can you recommend any refactoring tutorials or books that teach
| those kinds of lessons?
| leetrout wrote:
| Not specifically this, per se, but I HIGHLY recommend "A
| Philosophy of Software Design" by Dr. John Ousterhout
|
| https://web.stanford.edu/~ouster/cgi-bin/book.php
| mdaniel wrote:
| I wish I could upvote this a million times
|
| But, I'll also point out that just like _reading_ about
| exercise, merely reading the book doesn 't help unless one
| is willing to practice and -- much, much more difficult --
| get buy-in from the team. Because software engineering is
| usually a team sport and if one person is reading these
| kinds of books and trying to put them into practice, and
| the other members of the team are happy choosing chaos,
| it's going to be the outlier who gets voted off the island
| jprete wrote:
| Not the GP but I think a foundational skill is naming things.
| If you can't give a simple name to a function/class/etc.,
| it's probably not well-defined. It should be adjusted to make
| it easier to name, usually by moving responsibilities out of
| (or into) the code structure until it represents one concept
| that you can clearly state as a name.
| gozzoo wrote:
| This! Coming up with meaningfull names helps you undrestand
| the problem and define the solution. I advise junior devs:
| if you don't know how to name a variable give it simple
| 1-letter name: a, b, x, y. When you look at the code it is
| immediately clear how well they understands the problem.
| One should be careful to avoid the naming paralasys though.
| zombiwoof wrote:
| Super rock hard agree with you and disagree with the author
|
| I have seen so many terrible projects with methods with endless
| arguments/paramters, nested object parameters the signatures
| are fucking insane
|
| The biggest stench to me in any project is when I see a
| majority of methods all have > 6 arguments
|
| To quote Shoresy: so dumb
| arccy wrote:
| +1, have 2 implementations that each have an independent branch
| point? if you combine them you have a function with 2 bool
| parameters, and 4 possible states to test, 2 of which you might
| never need
| hinkley wrote:
| It's difficult to convince people that once you consider the
| testing pyramid, it's not just 2 + 2 + 2 < 2 x 2 x 2 but also
| 2 + 2 < 2 x 2
| silvestrov wrote:
| _" The greatest shortcoming of the human race is our
| inability to understand the exponential function"._
|
| https://en.wikipedia.org/wiki/Albert_Allen_Bartlett
| Kinrany wrote:
| The monstrosities with dozens of flags do not happen because of
| the first wonky parameter. Inlining a function or refactoring
| it when the third use case comes around and invalidates
| assumptions isn't hard.
| marcosdumay wrote:
| It depends. In fact the entire discussion is wrong, and neither
| rule has any real world value.
|
| People are all talking about the format of the code, while what
| defines if it's a good architecture or not is the semantics.
| Just evaluating that heuristic (yours or the article's) will
| lead you into writing worse code.
| KerrAvon wrote:
| This is really the issue with the article -- it's the CS
| equivalent of pop-psych feel-good advice like "write a page
| every day and you'll have a novel before you know it." It
| doesn't solve your actual problems. It doesn't solve
| anyone's. You're not actually better off in the long run if
| every line in your source is a separate commit, unless you
| have the world's most basic program.
|
| This "it's more important to wrap your code at 80 columns
| than to understand how the cache hierarchy works" stuff is
| becoming worryingly endemic. Teamscale has built an entire
| business around fooling nontechnical managers into believing
| this shit is not only worthwhile, but should be enforced by
| tooling, and middle managers at FAANGs, who should know
| better, are starting to buy in.
| srvaroa wrote:
| KISS > DRY
| deprecative wrote:
| DRY for the sake of DRY is like not drinking water when
| you're thirsty.
| AlphaSite wrote:
| Yep. Not all code that looks alike is alike.
|
| Similarity can be fleeting.
| hinkley wrote:
| The itch that Aspect Oriented Programming was trying to address
| was that some functionality only needs to differ by what
| happens in the preamble or the afterward.
|
| And that can be simulated in code you own by splitting the meat
| of a set of requirements into one or two bodies, and then doing
| setup, tear down, or a step in the middle differently in
| different contexts. So now you have a set of similar tasks with
| a set of subtasks that intersect or are a superset of the
| other.
| cpeterso wrote:
| These types of lookalike functions are like homonyms: they
| might be "spelled" the same, but they have different meanings
| and should not be conflated.
| simonw wrote:
| On commit size:
|
| > You just never know when you have to revert a particular change
| and there's a sense of bliss knowing where you introduced a bug
| six days ago and only reverting that commit without going through
| the savagery of merge conflicts.
|
| This is key for me: a good shape to aim for with a commit is one
| that can be easily reverted.
| charles_f wrote:
| A trick to help doing that, when you start having multiple
| changes that could be distinct commits, use _git add --patch_
| to select the changes one by one. Not only that can allow you
| to create smaller changes, it also gives you an opportunity to
| review your code before you commit
| JoshTriplett wrote:
| Agreed, but after decomposing the change into logical
| commits, doublecheck that the project builds after each
| commit.
| do_not_redeem wrote:
| Or even better, set up a pre-commit hook so that happens
| automatically.
| s4i wrote:
| Or even better, do that in CI.
| mdaniel wrote:
| As someone who works in small companies, and had to
| endure developers who were using gitlab as "offsite
| backup" or I guess "push-based 'does this compile?'
| workflow", please don't do this. CI minutes are rarely
| free, and for damn sure are not "glucose free". If you
| can't be bothered to run the local compilation step for
| your project, that is a wholly different code smell
| ervine wrote:
| Not for things like type / lint / formatting errors.
| Tests too if not too long.
|
| I mean have them in the CI as well, but for sure have
| them as pre-commit hooks.
| keybored wrote:
| Stalling a commit for more than a third of a second is
| way too much.
| ervine wrote:
| Slightly-longer commits to have never-broken commits...
| hmmmmmm.
| necovek wrote:
| Also look up at any one of the "stacked branches" approaches
| (plenty of git extensions or tutorials that work natively
| with newer git versions).
|
| For those still in bzr land, there used to be a wonderful
| "bzr-pipelines" plugin to enable seamlessly working on a set
| of interdependent changes.
| majormajor wrote:
| I've not seen "roll back a bug by reverting a single commit" be
| a viable option nearly as much as "roll back by manually
| changing the buggy part," especially for bugs six days old (or
| older).
|
| It's usually too hard, regardless of what your commits look
| like individually, to revert "just one buggy small bit" without
| breaking the rest of the new feature that was supported by that
| change, or re-introducing an old bug, or having other
| inconsistent resulting behavior. And "turn off the whole
| feature" is rarely desirable unless the bug is producing truly
| catastrophic behavior.
|
| A roll-forward "just fix that bug" is the ideal case. A more
| complex "roll forward and make a kinda involved fix" is common
| too. But neither of those regress things from a user or
| consumer POV.
| necovek wrote:
| Yeah, a rollback might be unfeasible for most things, but
| more "atomic" commits allow anyone handling an issue to
| better understand the reasoning behind any change, and if
| something was amiss in that particular change.
| patrick451 wrote:
| Unless all your features actually fit in one small commit, this
| doesn't work. Much more common is that you merge a chain of
| dependent commits, which means you cannot just rollback a
| single commit, since that will leave your codebase hopelessly
| broken. Much cleaner to commit the entire feature as one large
| commit.
| keybored wrote:
| You can rollback a merge if that is the goal of this one-
| large-commit.
| necovek wrote:
| If your "features" don't fit in one small commit, you should
| probably look to redefine what "features" are or at least not
| tie them to a commit.
|
| You can and should split your features into a series of
| product/codebase improvements that end up delivering the full
| "feature" with the last of your commits. If done smartly,
| along the way, you'll be delivering parts of the feature so
| your users would start benefiting sooner.
| thenoblesunfish wrote:
| You don't have to literally revert the commit, but it will make
| it easier to write commit to undoy plus aiming for this means
| your commits will be well-contained and reviewable, which is
| also good.
| mdaniel wrote:
| I agree with this, as well as the $(git add -p) suggestion,
| which JetBrains tools make super-duper easy, but my reasoning
| is not for reverts but for cherry-pick. I can count on one hand
| the number of meaningful reverts I've seen, but have
| innumerable examples of needs to cherry-pick. I admit that will
| heavily depend upon the branching style used in the project,
| but that's my experience
| keybored wrote:
| Cherry-pick is the copy-paste of VCS. And although copy-paste
| in code can work, copy-paste at the version control level
| itself is suspect if we're talking about long-term history
| (why copy the changes of a commit?).
| mdaniel wrote:
| There is a small distinction between copy-paste, which
| short of using static analysis tooling is undetectable,
| versus $(git cherry-pick) which is _tracked_ copy-paste
|
| Contrast: git checkout -b feat-1 echo
| 'awesome change' > README.md git commit -am'fix'
| git checkout main git checkout -b feat-2 echo
| 'awesome change' > README.md git commit -am'moar
| awesome fix' git checkout main git merge feat-1
| git merge feat-2
|
| with its cherry-pick friend
|
| If one is curious why in the world multiple branches would
| need the exact same commit, I'm sure there are hundreds of
| answers but the most immediate one is CI manifests are
| _per-branch_ so if one needs a change to CI, I would a
| thousand times rather $(for b in $affected_branches; do git
| checkout $b; git cherry-pick $my_awesome_ci_fix; done)
| which will survive those branches re-joining main
| keybored wrote:
| I try to do that for legibility and because it's easier to
| combine commits than to split them (that's just how git is).
| Revertability is pretty meh. It's nice when you get to revert a
| single commit and hotfix/solve the problem. But with these
| commit sizes you hardly save any time that way.
| jamietanna wrote:
| Related: https://news.ycombinator.com/item?id=40949229
| avg_dev wrote:
| i do think these are good habits. my favorite is the one about
| type #3 of tech debt. i wish i could push a button and impart
| this way of thinking to many of my old coworkers.
|
| (and, there is some room for taste/interpretation/etc. i think
| the thing about copy-paste and "the third time it's in the code,
| encapsulate it, and deal with flag params later" is maybe true
| and maybe not true and may be context or team dependent. i know i
| have done this a few times and if i am trying to cover that func
| with tests, the complexity of the test goes up fast with the
| number of flags. and then sometimes i wonder it is even worth
| writing these tests when the logic is so dead simple.)
| simonw wrote:
| "Know when you're testing the framework's capability. If you are,
| don't do it."
|
| Hard disagree on that. Frameworks change over time. How certain
| are you that they won't make a seemingly tiny design decision in
| the future that breaks your software?
|
| One of the most valuable things tests can do for you is to
| confirm that it is safe to upgrade your dependencies.
|
| If all your test does is duplicate tests from dependency that
| might be a waste of time... provided that's a stable, documented
| feature and not something that just happens to work but isn't
| necessarily expected stable behavior.
|
| But you shouldn't skip testing something because you're confident
| that the dependency has already covered that.
|
| The tests should prove your software still works.
| ajmurmann wrote:
| I very much agree with you on this. Upgrading dependencies is
| something you do and you are responsible for if it broke
| things. I'd frame it slightly differently though. I think you
| should have some tests that tests the full functionality the
| user will experience, regardless where the pieces come from.
| And don't go our of your way to mock or stub something because
| it's not written by you. There is no reason to avoid useState()
| being executed in your test suite as long as your code actually
| depends on it and your test doesn't get super expensive to
| execute or write because of it. Now, if something is expensive,
| try to limit testing it only to the top of your testing
| pyramid. But you should till test the full stack because that's
| what you are gonna ship!
| ervine wrote:
| I think it probably is saying: don't write a "useEffect runs
| when its dependencies change", write a "User is redirected to
| their accounts page after loging in", and you are testing both
| your own code and the framework's routing / side effects
| handling / state tracking, etc.
|
| Integration tests for complex flows inadvertently tests your
| dependencies, which as you say is awesome for when you have to
| upgrade.
| vander_elst wrote:
| > It's better to have some wonky parameterization than it is to
| have multiple implementations of nearly the same thing. Improving
| the parameters will be easier than to consolidate four different
| implementations if this situation comes up again.
|
| From https://go-proverbs.github.io/: A little copying is better
| than a little dependency.
|
| Curious to see how the community is divided on this, I think I'm
| more leaning towards the single implementation side.
| OtomotO wrote:
| I decide on a case by case basis.
|
| I've been bitten by both decisions in the past. Prematurely
| abstracting and "what's 4 copies gonna do, that's totally
| manageable" until it cost quite some time to fix bugs (multiple
| times then, and because of diverged code paths, with multiple
| different solutions)
| ulbu wrote:
| I think an abstraction should imply/enforce a common abstract
| structure. It inscribes an abstraction layer into the system.
| Moving a couple of concrete lines into a single named scope
| is orthogonal to this.
| abound wrote:
| Like most things, blanket advice will break down in some cases,
| things can be highly contextual.
|
| Generally, my anecdotal experience is that Go libraries have
| far fewer average dependencies than the equivalent Rust or
| JavaScript libraries, and it may be due in part to this (the
| comprehensive standard library also definitely helps).
|
| I definitely tend to copy small snippets between my projects
| and rely sparingly on dependencies unless they're a core part
| of the application (database adapter, heavy or security-
| sensitive specifications like OIDC, etc)
| horsawlarway wrote:
| The older I get, and the more experience I have, the more I
| think "single implementation" is generally a lie we tell to
| ourselves.
|
| To the author's point - a wonky param to control code flow is a
| clear and glaring sign that you consolidated something that
| wasn't actually the same.
|
| The similarity was a lie. A mistake you made because young
| features often have superficial code paths that look similar,
| but turn out to be critically distinct as your product ages.
|
| Especially with modern type systems - go ahead and copy, copy
| twice, three times, sometimes more. It's so much easier to
| consolidate later than it is to untangle code that shouldn't
| have ever been intertwined in the first place. Lean on a set of
| shared types, instead of a shared implementation.
|
| My future self is always happier with past me when I made a new
| required changeset tedious but simple. Complexity is where the
| demons live, and shared code is pure complexity. I have to
| manage every downstream consumer, get it right for all of them,
| and keep it all in my head at the same time. That starts off
| real easy at shared consumer number 2, and is a miserable,
| miserable experience by consumer number 10, with 6 wonky params
| thrown in, and complex mature features.
|
| ---
|
| So for me - his rule of thumb is egregiously too strict.
| Consolidate late and rarely. Assume the similarity is a lie.
| normie3000 wrote:
| Alternative to #10: avoid mocking.
| mdaniel wrote:
| I believe there is nuance to this: how else would any sane
| person exercise _error_ flows in software, or -- as I have
| personally implemented -- test against things which are wallet-
| expensive in real life?
|
| What I oppose is mocking every single dependency of every
| single injection in the component. It ends up being 50x the
| code of the system under test and requires throwing it all away
| when the implementation changes
| necovek wrote:
| Unfortunately, most "frameworks" in existence today do not
| follow a simple, functional design, and they tend to make you
| mock quite a bit.
|
| But the alternative to "mocking" is to use verified fakes
| (same test passes for both the real implementation and the
| fake) that actually do something "real" (even if it's simply
| persisting data in memory).
| sgarland wrote:
| > [ignore] things that might prevent you from doing stuff later.
|
| This only works if you know what is and is not a potential future
| blocker. A perfect example is the data model: IME, most devs do
| not understand RDBMS very well, and so don't understand how their
| decisions will affect future changes or growth. Or worse, they
| recognize that they don't know, but choose to dump everything
| into a JSON column to avoid migrations.
| zombiwoof wrote:
| Seems like the definition here of software is always
| "maintenance" of something as is, like replacing the boards on
| Theseus
|
| Sometimes software is hard and 10x engineers just need to rewrite
| the whole thing or replace large systems
|
| To subscribe to some world where we have to do that in "small
| changes" limits us
|
| We shouldn't make process to the weakest engineers
| majormajor wrote:
| Even if you're a "10x engineer" the ability to _describe_ how
| you would fix or replace things using just small changes is
| extremely valuable. And the inability to put together a
| moderately-detailed plan for that is a big smell.
|
| If you don't actually understand the full set of changes that
| will be required in order to get to your desired new end state,
| how can you evaluate whether "just write the whole thing" is a
| one month, six month, or longer project? There are going to be
| nasty edge cases and forgotten requirements buried in that old
| code, and if you discover them for the first time halfway into
| your big rewrite... you might suddenly find you're only 10%
| into your big rewrite.
|
| ( _Especially_ if you 're a "10x engineer" you should
| understand what makes big rewrites hard and often fail or go
| way over schedule/budget. You should've seen it all before.)
| alganet wrote:
| Why rewrite then? We should have only the strongest engineers,
| only those able to understand and thrive in any kind of
| spaghetti.
| adamredwoods wrote:
| I've dealt with both: 1. maintenance coding 2. re-write coding
|
| Re-writes take forever, because a lot of the edge cases and bug
| fixes are lost [1]. You might think they go away, and some do,
| but new ones are introduced. QA process is critical. Management
| becomes critical of excuses, and the longer the project is
| drawn out, the more they get involved. The final shift to a new
| system is never one-and-done. Management is paying for two
| systems, canary deploy.
|
| Smaller re-writes are the ideal practice, and your code base is
| set up this way already, right?
|
| Maintenance code is cheapest.
|
| [1] https://www.joelonsoftware.com/2000/04/06/things-you-
| should-...
| necovek wrote:
| My experience tells me that it's both _faster_ and _higher
| quality_ to do things in small steps than leave it with your
| "10x engineers" (everybody thinks they are the one, right?) to
| "just" rewrite from scratch -- and I've got plenty of proof in
| my close-to-20-years of career (I've never seen that go smooth;
| I've been a part of dozens of iterative "replace large systems"
| that were pretty uneventful).
|
| As for the "weakest" engineers, even the "strongest" engineers
| are weak sometimes (bad day, something personal, health issues,
| sleep deprivation...).
| alexchamberlain wrote:
| I think it's misleading to say iteration or full rewrites are
| the only 2 options. The most impactful, yet successful,
| projects I've worked on rewrite a part of a system. ie replace
| a custom search index by Solr, but leave the data itself and
| the UI the same, then once you're happy that went well, improve
| the data or the UI afterwards.
| chipdart wrote:
| From the article:
|
| > Copy-paste is OK once. The second time you're introducing
| duplication (i.e., three copies), don't. You should have enough
| data points to create a good enough abstraction.
|
| There's already a principle that synthesizes this: Write
| Everything Twice (WET).
|
| It's a play on words to counter the infamous Don't Repeat
| Yourself (DRY) principle, which clueless but opinionated
| developers everywhere have used time and again to justify
| introducing all kinds of problems involving a combination of
| tight-coupling unrelated code, abstraction hell, adding three
| classes and an interface to avoid writing two classes, etc. This
| nonsense is avoided by tolerating duplicate but uncoupled code
| until the real abstraction and coupling needs emerge.
|
| I still cringe at a PR that a former clueless junior developer
| posted, where in the name of DRY added a OnFailure handler which,
| instead of doing any error-handling and recovery logic, simply
| invoked OnSuccess, because "it's mostly duplicate code and this
| keeps the code DRY". Utter nonsense.
| atoav wrote:
| Software development is simple, try to maximize all of these at
| the same time:
|
| 1. Performance
|
| 2. Reliability
|
| 3. Readability
|
| 4. Correctness
|
| 5. Maintainability
|
| 6. Extendability
|
| 7. Consistency
|
| 8. Adequacy
|
| 9. Simplicity
|
| 10. Predictability
| majewsky wrote:
| We are all in agreement here. This entire comment section is
| just about the coefficients for the objective function.
| hugodan wrote:
| reads like a chatgpt answer
| vrnvu wrote:
| "Know when you're testing the framework's capability. If you are,
| don't do it. The framework is already tested by people who know a
| lot more than you."
|
| How many times have you had to roll back a minor version upgrade
| because the library maintainers *absolutely don't* know what they
| are doing? Spring, Netty, and Java ecosystem, I'm looking at
| you...
| ervine wrote:
| next.js, apollo client... so many surprises even in minor point
| versions.
| Barrin92 wrote:
| Pretty substantial disagree with the second half of 4. and 5.
|
| >If the component is big, then you introduce more complexity[...]
| If a particular function doesn't fit anywhere, create a new
| module (or class or component)
|
| This smells like the agile/uncle Bob "every function should be
| four lines" school of thought which is really bad.
|
| Paraphrasing Ousterhout's book, it's the other way around, when
| components are big and contain significant implementation you're
| _hiding information_ and reducing complexity, which is the
| purpose of good program design. When your component
| /object/module is just surface you've basically done no work for
| whoever uses your code. I see it way too often that people write
| components that are just thin wrappers around some library
| function in which case you haven't created an abstraction, you've
| just added a level of indirection.
|
| If a function does not fit anywhere that's a strong indication
| that it shouldn't be a separate function, it's likely an
| implementation detail.
| brewmarche wrote:
| Are you talking about this book: A Philosophy of Software
| Design? Can you recommend it?
|
| I am looking for rebuttals of this naive Uncle Bob style and
| while I like the content of Casey Muratori, he doesn't resonate
| with more corporate people.
| G1N wrote:
| > Copy-paste is OK once. The second time you're introducing
| duplication (i.e., three copies), don't. You should have enough
| data points to create a good enough abstraction. The risk of
| diverging implementations of the same thing is too high at this
| point, and consolidation is needed. It's better to have some
| wonky parameterization than it is to have multiple
| implementations of nearly the same thing. Improving the
| parameters will be easier than to consolidate four different
| implementations if this situation comes up again.
|
| The more I do this software engineering thing the more I feel
| like this "advice" bites me in the butt. Understanding when you
| should duplicate code versus when you should consolidate (or if
| you should just write a TODO saying "determine if this should be
| split up by [some set in stone timeline]") is simply just a HARD
| problem (sometimes at least), and we should treat it as such.
|
| DRY/ WET or whatever shouldn't be a maxim (let alone a habit!
| lol), it should at best be a hand-wavey 2-bit dismissal you give
| an annoyingly persistent junior software dev who you don't want
| to actually help!
| jjice wrote:
| I see what you mean. DRY and WET and similar ideas are
| delivered as objective sometimes, but I think it's better to
| view them as general heuristics, as most rules in software
| should be.
| majorbugger wrote:
| I don't get the part about the small commits. To me a commit
| could be massive and that's alright, provided it introduces some
| major feature, while a fix could a one-liner. It really depends
| on the situation.
| ajmurmann wrote:
| It makes debugging so much easier to have small, atomic
| commits. Of course what's viable depends on what you are doing.
| I've had great success making changes and rolling them out that
| aren't actually the full feature yet and some or all parts
| remain hidden. This also can alleviate the race between two
| large changes coming in and having to deal with merge
| conflicts.
| RangerScience wrote:
| Large commits are (IMO) a symptom - lack of a plan, a plan that
| doesn't work out, etc. Which is fine! You have to figure it all
| out _somewhere_.
|
| One thing you can do to address them is to stash the large
| commit to the side, then piece by piece pull it into a new
| branch as a series of smaller commits. This also give a good
| opportunity to refactor before delivery, now that you know what
| the code is going to do and how.
| necovek wrote:
| This means that you should look to break up a "major feature"
| into smaller, iterative steps to delivery.
|
| In general, the biggest hurdle engineers need to overcome is to
| believe it is possible and then simply start thinking in terms
| of delivering value with every single branch (hopefully user
| value, but a refactoring counts too), and what are the small
| steps that get us there?
|
| The benefits are amazing:
|
| * Changes are likely to be limited to only one "thing", thus
| making them both lower-risk and easier to review and QA
|
| * With every step shipped to production, you learn if it is
| providing the benefit you are looking for or if you need to
| pivot
|
| * You are not developing a feature branch while "main" moves at
| the same time, and wasting time on keeping up with it
|
| * If the project gets stopped 3 months in, you have still
| delivered some value, including those in-between refactorings
|
| * Your customers love you since they are seeing improvements
| regularly
|
| * There is never any high-risk, big "release" where you need to
| sit around as 24/7 support and wait for bugs to rear their
| heads
|
| I am happy to give some guidance myself: what is the "major
| feature" you think can only be done with a single, large change
| all at once? (I've done huge DB model changes affecting 100Ms
| of rows with no downtime, merged two "subapps" into one,
| migrated monoliths to microservices etc, but also built new
| full-stack complex features with branches with diff size being
| less than 400 lines for each)
| tripple6 wrote:
| Having a massive major feature done as a single commit is evil.
| Merging two branches may conclude combining a unit of work, a
| major feature, a minor feature with the main branch (of course
| once the topic branch is merged to the upstream, and never vice
| versa [rebase in git terminology]). This is logically "a big
| commit" constructed from a concrete amount of small commits.
| Additionally, having small atomic commits also makes reverting
| a commit a trivial operation regardless the branch the commit
| was introduced in. Bisecting a range of small commits also
| makes finding a bad commit easier.
| lifeisstillgood wrote:
| There is this dichotomy - companies say they want stable codebase
| with clear justifications for each chnage (at least heavily
| regulated companies do).
|
| But good practise here is continual refactoring - almost
| inimicable to that stability plus imagine the final sign off
| comes from business who don't understand why you rewrote a
| codebase that they signed off two months ago and now have to re-
| confirm
| Scubabear68 wrote:
| "Aim for at least half of all commits to be refactorings".
|
| I feel like this is the end game of scrum and most agile
| methodologies - endless refactoring on a treadmill with no off
| button,
|
| I like to be introspective, and I am human so my code is far from
| perfect. But if I was refactoring half of my time I would go more
| than a little crazy.
|
| The good systems I have worked on have converged on designs that
| work for that space. Both developers and users see and value the
| stability.
|
| The bad ones have had the kind of churn the article mentions.
| Developers are constantly rewriting, functionality is subtly
| changing all the time; stability doesn't exist.
| revskill wrote:
| Good code is an asset.
| henning wrote:
| No.
|
| > Know when you're testing the framework's capability. If you
| are, don't do it
|
| Except that many frameworks are full of confusing behavior that
| is easy to misuse. It's funny that the post mentions
| `useEffect()` because `useEffect()` is so easy to misuse. Writing
| integration tests that make sure your app does what it is
| supposed to is totally fine.
|
| > If you don't know what an API should look like, write the tests
| first as it'll force you to think of the "customer" which in this
| case is you
|
| This is pointless. It doesn't give you any information, you're
| just guessing at what the API should look like. You won't
| actually know until it's integrated into a working application.
| The idea that you can design in a vacuum like this is wishful
| thinking.
|
| > Copy-paste is OK once. The second time you're introducing
| duplication (i.e., three copies), don't. You should have enough
| data points to create a good enough abstraction.
|
| No you won't, and it will often be with code that is similar in
| some ways but differs in others. Since the kind of people who
| write this kind of vague bullshit advice disapprove of things
| like boolean function parameters and use shitty languages that
| don't have metaprogramming support, this leads to "abstractions"
| that create awkward, tight coupling where changing one little
| thing breaks a million stupid fucking unit tests.
|
| > Testability is correlated with good design. Something not being
| easily testable hints that the design needs to be changed.
|
| Testability is neither necessary nor sufficient for any
| particular quality attribute. Depending on the application being
| written, it can be counterproductive to write out full unit tests
| for everything.
|
| As always with these stupid "software engineering" posts, there
| is zero data, zero evidence, zero definitions of terms up front,
| and zero of anything that is actually real. It's just personal
| preference, making it dogma.
| necovek wrote:
| I challenge you to write code that is "testable" (easy to cover
| with tests for all the important functionality), but which is
| generally badly designed and structured.
|
| (FWIW, while naming is probably as important, I am not
| accepting bad naming as that is too easy)
| layer8 wrote:
| > Copy-paste is OK once. The second time you're introducing
| duplication (i.e., three copies), don't. You should have enough
| data points to create a good enough abstraction. The risk of
| diverging implementations of the same thing is too high at this
| point, and consolidation is needed.
|
| This heavily depends on how likely it is for the reasons of
| change to also apply to the other copies. If the reasons for why
| the code is the way it is are likely to evolve differently for
| the different copies, then it's better to just leave them as
| copies.
|
| Just being the same code initially is not a sufficient reason to
| create an abstraction. Don't focus on the fact that the code is
| currently the same, instead focus on whether a change in one copy
| would necessarily prompt the same change in the other copy.
|
| This also applies to pieces of code that are different from the
| beginning, but are likely to have to change in conjunction,
| because they rely on shared or mutual assumptions. If possible
| place those pieces of code next to each other, and maybe add a
| source comment about the relevant mutual assumptions.
|
| In other words, avoiding code duplication is a non-goal. Keeping
| code together that needs to evolve together is a goal. Instead of
| DRY or WET (don't repeat yourself, write everything twice), think
| SPOT (single point of truth).
| devjab wrote:
| The only absolute rule that you'll ever need is that you
| probably won't need the abstraction you're thinking about. To
| be frank though, it started with putting a function into a new
| module or class. I think the list is rather bad as a whole.
| It's the same as a lot of other "best practices". It's vague
| enough that you can't really use it, but also so that you can't
| really fault it.
|
| Copy pasting code multiple times is never really "fine". I'd
| argue that for most things you'd probably be better off writing
| a duplication script rather than abstracting it into some over
| complicated nonsense. It's much easier to change, and delete,
| things later this way. It's obviously not what we teach in CS
| though, but we really should.
| silvestrov wrote:
| My favorite anti-example is year based tax calculation.
|
| Rules can change enough from year to year so that parameters
| isn't enough. You will end up with code specific for each year.
|
| You don't want to introduce any chance of changing results for
| old years when changing common code.
|
| So best to have no common calc code. Each year is fully set in
| stone.
| tegiddrone wrote:
| > 5. If a particular function doesn't fit anywhere, create a new
| module (or class or component) for it and you'll find a home for
| it later.
|
| I worked at a place that did this with their frontend app. Devs
| rarely knew where anything should go and so for any given
| Component/Module, there was usually some accompanying
| `MyComponent.fns.ts` file. Homes were NEVER found for it later.
| Code duplication through the nose and lots of spaghetti coupling.
|
| Edit: i'm definitely blowing off some steam. That said, I think
| there is good virtue in this "habit" so long as there is good
| reason that it "doesn't fit anywhere" ... and when another module
| starts referencing the temporary home module, it is a smell that
| the time is now to give it a proper home.
___________________________________________________________________
(page generated 2024-11-17 23:00 UTC)