Code Lifespan
xkcd.com
xkcd.com
I don't think it's quite as widespread, I would posit that this has changed to: - (1) YAGNI, build the simplest solution you can for the problem at hand. That will make it as easy as possible to change when future requirements are presented. (2) Design your code to be replaced, not extended. Design your modules in a way that someone can rewrite one without having to rewrite the whole system.
Points (1) & (2) really contradicts the generalized solution approach. The generalized solution tends to be very abstract, hard to understand, and because there are layers - not very modular (which makes it hard to re-write)
I would disagree that "generalized" equates to simple. Generalized means generic and usually abstract.
- https://en.wikipedia.org/wiki/Rule_of_three_(computer_progra...
- https://blog.codinghorror.com/rule-of-three/ "It is three times as difficult to build reusable components as single use components"
I think implicit in the point, most code does not go on to be used thousands of time (let alone dozens or even more than once). Even if some code is used more than once, making it the same and shared has significant costs as well.
So I was told this as bluntly as possible by someone 15 years ago - "you don't have to code all parts of your design".
Every line of code you write makes it harder to replace it. That the feature is an asset, the code is a liability. That the goal they had set out for me was to write as little as possible, but achieve the same result.
The only reason to rewrite a system out of tech debt, which is what I was doing then & getting lost in a sort of second system "this is the last rewrite" effort, is to write the next feature with less code - even if the next system needs you to rewrite bits you just wrote.
This might sound simplistic, but I've seen that work over and over again.
Plus, it was much easier to hand over the code to the next person when I wanted to do something new and interesting, if it was functionally complete and not full of half designed extension points all over.
That's an excellent articulation of what I think as "invisible seams" when I write code: they're soft points of extensions that don't need a separate interface/function/class _yet_. Sometimes I just mark them for myself with an extra newline within a function.
I would say that's more false then true.
Why? Because the design you haven't coded is a fantasy design. I'll give you a counter-quite, and excuse the military context: "No plan survives contact with the enemy" (simplification of a longer quote by Helmuth von Moltke.)
I have yet to make any non-trivial design which does not require significant changes while it is being coded. Perhaps some design geniuses with much deeper insight can make this happen - but those are very few. Most people probably need several rounds of implementation and user interaction before reaching an acceptable design.
So those parts of your design which you haven't coded are likely partially irrelevant and un-implementable.
This is not to say that you should write huge amounts of code. But - try to avoid designing much beyond what you actually code.
I still find it funny that the place to optimize is generally for 'rip-out-and-replace', rather than 'extend' (caveat emport, not always the case, but seems to be a better design to optimize for replaceable than it is to optimize for abstract & extensible)
Indeed. There is a balance to be found. Over-engineering and using the wrong abstractions hurts. Working with an “organic design” that has no useful separation of concerns, or forever reimplementing the basic data structures and algorithms of whatever domain you’re working in instead of doing creative work that is going to solve your real problem, also hurts. Too far towards either extreme and development progress slows to a crawl while quality plummets.
I wish we had a bit less emphasis in the industry on always using the latest shiny tools and a bit more on learning good fundamentals like data structures, algorithms and design patterns. Of course abstractions sometimes need to evolve as requirements do, but identifying which abstractions are helpful at present or when an existing abstraction isn’t helping any more and should be removed or replaced is absolutely a skill that can be developed with knowledge and experience, and it pays huge dividends to those who cultivate it.
Now we advise people to build the simplest and most obvious version for the problem you are looking at right now and if in the future you need it all over the place, replace it with a reusable one.
The exception seems to be React components, those generally seem to be very easy to identify as reusable UI and can be made generic from day one.
There's the problem.
Depending on the use case, this may not obviate the necessity of plain-text/wiki documentation; but I do note a tendency for many wikis, documentation web sites, etc, to be incomplete and/or woefully out of date. Half-assed docs can almost be worse than no docs; if it’s a priority, then it has to be folded into the entire SDLC, or have its own dedicated staff.
If you write clean code that is easy to groc and easy to replace, then its also easy to extend once you know where you want to extend to.
This is true for systems where you already have working code and you have multiple concrete examples of code that would benefit from refactoring or rewriting a reusable module.
I think the skepticism is more toward the idea that you should design for reuse up front before you have working code. There are two failure modes. One is that you spend time making something reusable that you only end up using in one place. The other is that you bake assumptions into the code that later turn out to be false. In both cases, the resources spent on reusability could have a net negative impact on the project.
You might be able to do that. That code (most likely) still needs to solve a complex issue and needs documentation to survive next to it. Also, there is a good chance that the next guy coming to fix it is not going to investigate the extra time to fully understand the code if he can fix that one crash by throwing in a hotfix at the specific line accessing a nullpointer. Lastly, at some point, if you need to solve a difficult problem, the code to do so is likely going to be more difficult to read, too.
It's not impossible to do this, but having the time to do a clean solution, having the foresight to be right about the necessary complexity and also have the environment for it to stay clean is simply rare.
When I try to build similar abstractions over business processes, they typically don't survive the next wave of changing requirements.
I concur. If we look at abstractions that have stood the test of time, they are often relatively simple to describe, but they express powerful ideas that are widely applicable. A few examples come to mind:
• Structured programming
• Iterators and generators
• Promises
• Atomic transactions on databases
• Model-view-whatever patterns in UI code
• Regular expressions
• Pipelines
• Continuations
• Haskell’s standard typeclasses (particularly Functor, Applicative, Monad, Monoid, Foldable and Traversable)
Many of these have proved so useful that popular languages now directly incorporate them or perhaps, for the most abstract ones, incorporate features that are more specific instances of the general idea.
I think the last example is particularly telling. The patterns Haskell developers work with are in one sense very abstract: each is ultimately just a short list of mathematical properties that some type can have. This turns out to be quite a high barrier to entry and understanding their true nature is something of a rite of passage that many fledgling Haskell developers never complete. It also took some very smart people many years of thought and discussion before the community eventually reached the list above.
However, once you do recognise the patterns, you see them everywhere. They create some very clear seams in your software design that allow separation of concerns and composability to a degree that less powerful abstractions only dream of. And because they’re rooted in relatively simple properties, they’re about as stable and future-proof as anything in programming can be.
Maybe one day they’ll be supplanted by a more effective abstraction with better language support. Effect systems, perhaps? But for now, you can express ideas in a few lines of Haskell that would take 10x that in many other programming languages, because you can compose tools from a vast toolbox of data structures, algorithms and design patterns in almost arbitrary ways. But you have to learn some abstractions that aren’t widely known and have silly names first. :-)
> I have more success writing reusable libraries that do very, very fundamental stuff
Same here. Things like logical and mathematical operators. Memory access and stuff.p.s. I also wrote that same library when go generics landed.
Putting something behind an interface / abstract factory / whatever ironically often makes it harder to change because you have to read more lines of code before you can start modifying the program. Programs with a lot of lines of code are intimidating.
There are situations where you have clean module boundaries, which lets you separate your code into separate independent modules. (Eg POSIX separating user applications and libc / the kernel). But the downside is that once you've separated your code into modules (with documented APIs) the module boundary itself becomes harder to change. You're essentially betting that you have the right API. You lose flexibility at the boundary in exchange for flexibility in the code on either side of that API.
But all that said, I don't think there's any way to learn this stuff without experience. "The dao that can be spoken is not the eternal dao". There's no set of rules for this stuff that can be explained simply, and which will give you the ideal outcome in all situations.
I think the “secret” is proportionality. A useful abstraction hides significantly more complexity than it introduces. That could be a single function, if that gives a name to a simple but common algorithm on a particular data structure like map or reduce. Or it could be a 100 function API with 100,000 more lines behind it, if that provides a correct, efficient implementation of some entire field of programming that your application rests on. But it’s probably not a one-line function that needs a longer name to describe what it does in words than its whole implementation, and it’s probably not a library with a 100 function API that only obfuscates a few standard data structures and custom types that are locked away in the implementation for little benefit.
Thanks for expressing this - I think this is a fantastic insight. I love C as a language, and I've been really enjoying Rust lately too. Both languages make it easy to design around simple, raw structs. And a simple struct with 3 public fields is often a much easier interface to work with than 100 getter & setter methods hiding the same struct which has private fields.
But in 20 years...
1. It is many people's thing, not your thing.
2. It is inter-dependent with code you can't rewrite 5 times
3. You're probably no longer there anyway.
So I'll have the generalized solution, thank you. Of course, it's even better if you can use an already well-established FOSS solution, since that's likely to see improvements over time which may be applicable to your code.
We have a CSV parser library in our codebase. It’s probably from 2005 or so. I’m sure it was a good idea at the time but at this point it has resisted replacement because of dozens of legacy reports that rely on bugs or quirks in it (mostly around quoting).
All the other dev leaders hated it. They said it was too complex and they couldn't iterate on it quickly enough. It took one of them 2 hours to change a fundamental bit of it!
They then (after I left that position to work on the cryptography) proceeded to rewrite the entire thing using domain oriented design. It was a beautiful set of interfaces. It was about 100 times less efficient (running on mobile phones). It took them 6 months to rewrite it.
I figure that everyone just has their preferred style. The right amount of abstraction is whatever you wrote!
I am always shocked when I learn the some code I wrote as throwaway-tp-be-done-right-later 10 years ago is still in use.
That's because your "quick throwaway" code solves a real problem. It is useful, it works, it is very likely easy to understand.
In short: It's good code.
(It might not be "elegant" or "clever" or even "neat". Those things are overrated.)
> It is useful, it works, it is very likely easy to understand.
It probably mostly solves the intended problem, and might be so close to coming apart that nobody dares improve it.
>Guys I’ve been stuck with this application that says its a web app doesn’t render in the browser engine. It has its own version of a browser engine that’s from thirty years ago and it needs 200 dependencies from a repository that doesn’t exist anymore. How am I supposed to maintain this?
(Note that I left 30% for some of them to do some good, occasionally.)
It's an economic endeavor, not an artistic one. I've been in some choruses, and it's really worth it to make every single thing as good as you can possibly make it. The audience for the concert appreciates the totality of what you do.
A coding project is not art like that. It's not worth making most things perfect, and moreover, you can't tell now which ones are going to be worth it.
Every hour you spend on polishing something that's going to be thrown away in two years is an hour you can't be doing something more valuable. The Net Present Value of extra work five years from now is almost zero.
If you actually sit along and see the whole process you can actually understand the whole piece of work as well as give suggestions when they are useful and much cheaper to action.
That sounds like more of a criticism of large PRs and long feedback cycles than of code review itself.
But code reviews have at least one other way of contributing value. They help find (moderately) obvious bugs, unsafe code, and unclear code. There is value in keeping blatant stupidity out of your codebase.
I said you get the second order benefits by establishing shared ownership. Of course the qualities of the code are conferred to callers… that’s obvious.
Simplifies to: Code is never reused (99.99% probability)
> Let's not overthink it [... ==> ...] code lives forever.
Simplifies to: Reused code is always underwhelming (99.99% probability)
Solution? This is the optimum! Tuning 0.01% of situations isn't a good use of time. Stop worrying and learn to love the spaghetti!
+---------------------------------+
| There is nothing more permanent |
| than something temporary |
+---------------------------------+Engineers frequently have trouble with this. They place the code on the wrong side of the balance sheet. They place it on the assets side. It's on the liabilities side.
I heard it wasn't retired until 10 years after I wrote it (which was about 6.5 years after I left).
Working in an organization where there's a lot of overengineering going on constantly and it definitely feels like a significant portion is wasted effort for the time horizon such projects survive for.
Randall Monroe observations for this kind of stuff are always really spot on. I wonder what his creative process is like. I should probably read XKCD more often...
If you’re being screamed at to deliver asap and having to take shortcuts, that’s when your short term MVP proof of concept gets embedded unless you have someone really good watching over you
Change is hard, starting again is more fun.
It is fun, as we "know" that "build one to throw away" is wise advice. We still find every way possible to make it hard to throw what we are working on away.
"Niets is zo permanent als een tijdelijke oplossing."
The most durable thing is makeshift.