[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)