Simplicity Before Generality, Use Before Reuse
medium.com
medium.com
Yeah, but otoh, most progress in science and tech has been made by sort-of-tricking people into paying for stuff that solve a slightly more interesting/general problem than the original boring problem they wanted solved.
So at least when it doesn't drown everyone in paralyzing complexity, we need to remember to keep that "hacky rascality" alive and slightly "screw" our clients at the expense of developing some general and reusable solutions (hopefully as open-source libraries :P) that will probably matter more than whatever "important" problems they want solving ;)
I'd say there's no great sin in solving a slightly more general problem than what you currently face. In fact, that's often net simpler almost immediately; it's not long before a function to format a URL is simpler than simply manipulating a URL every time. (Even ignoring the simpler issues.) The problem arises when you have something like "Let's see... I need a database... let's write an ORM." Whoops. (Speaking from harsh experience there.)
I sometimes hear, from other engineers working on a complex React app, that the point of React’s components is that every component is written to be “reusable.” Every component. This is used as a justification for “going general” immediately when writing a component. In reality, there is real work to make a one-off component into a reusable component, and that work can only be done in the context of having (or at least realistically imagining, not dreaming up as an exercise) multiple use sites.
What React components actually provide are the same things that modules and functions provide: encapsulation; limited dependencies; a calling convention. When you write a function, you try to keep the arguments to a minimum; you might say it makes the function more “reusable,” but there is still a big difference between a library function that is designed to be used from anywhere, and a private helper function that is not. React components are “reusable” the way functions and modules are reusable. A better word might be “usable.” :) You can reason about the use, even if there is only one use.
Making something “reusable” means making it general, and we’ve all had the experience that the article points to of taking some code that was intended to be general, but in fact is used in only one place, and remaking it so that it’s actually possible to use in a second place.
That said, I have many times in programming succeeded in boiling something down to its essence in advance, and writing the general thing first, followed by the specific thing. I suspect this is actually more about simplicity than generality, however. I’m thinking of cases of “bottom-up” development where you write the primitives and then the higher-level parts in terms of the primitives. Actually, maybe it is about generality too, and when you are testing out your primitives in different combinations, you are doing the work required for good generality (seeing lots of different cases and basing your decisions on that).
More and more, these articles remind me of schoolyard wisdom, the kind people love to throw around because they sound wise. "Winners never quit." "Quitters never win". "Quit while you're ahead". Taken individually, they all sound perfectly reasonable and wise. Put them together, and clearly there's something wrong.
This article strikes me in the same vein. "YAGNI. Keep your code as simple as possible. Focus on your code as it is right now, and not what's going to happen in the future". "The only constant is change. Business requirements are always in flux. The mark of good code is the ability to change its behavior without overhauling everything". Taken individually, they both sound perfectly reasonable, but put them together, and something is clearly wrong.
Maybe it's time we accepted that there is no one right way. No one all-important metric or rule that trumps everything else. That simplicity is important, that conciseness is important, that DRY is important, that being able to adapt to future changes is important. That good code finds a balance between all of the above, without sacrificing one on the altar of another. That instead of writing articles trying to elevate one over another by attacking strawmen, we should instead present realistic code examples, thorough which we can have a grounded discussion on how best to find the balance between all the different qualities that comprise good code.
It's not going to be as easy or fun as simply declaring "X considered harmful", but it's going to help elevate the quality of our discussion, and help our profession progress to the next level.
Which makes sense; the former is a rebuttal of the latter. The C or Go philosophy is fundamentally at odds with the C++ or Haskell philosophy.
traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)
is more generic than traverse :: Int -> IO Float -> [Int] -> IO [Float]
and therefore strictly better (unless there are reasons the latter could be optimized but not the former).I think that especially for application code (as opposed to library code), it is a mistake to make your type signature far more generic than you need right away, especially because it is usually easy to switch to the more generic type declaration later without changing much existing code.
traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)
mySpecialTraverse :: Int -> IO Float -> [Int] -> IO [Float]
mySpecialTraverse = traverse
in some instances.> How much of what software engineers now do is still devoted to the accidental, as opposed to the essential?
Far, far too much
My personal philosophy is to keep the dependencies to a minimum and make judgement calls on simplicity versus generality. I probably err on the side of simplicity a little too often, but I also get asked to deploy solutions on non-internet connected devices fairly often. Solutions that you can deploy out of a tarball on a base CentOS 5.5 machine are good sometimes.