Lazy programming
I've been following an experiment of what I would call "lazy programming" in my current project. It turns out to be quite practical, so I'm going to try to summarize it in this post for a future reader, including myself.
Throw away these abstractions
When the project grows above a certain size, it's common to start forgetting how certain pieces of the code work, so you need to read them. Complex abstractions just obscure that clarity I think. So my position is to write less abstractions, not more. Perhaps using a set of simple ones only - composable functions, builder patterns, etc. You can get very far with these. Keep the logic right there, not buried away, requiring navigating through tens of files to find the relevant code. In other words, inline code as much as practically possible. I call that dense code. It's about being able to read something almost top to bottom like an article.
For comparison, the server side of my project is currently 27k lines of code split between 70 files, that's almost 400 lines per file. One project I took for comparison has 7k split between 150 files, almost an order of magnitude difference. It's like you have to maintain the stack of all these calls in your head while browsing these files. Turns out what is simple for the computer is simpler for the human too. You will be happy reading the denser code in the future!
Note that simpler abstraction does not equal lack of modularity. Keep the code organized between subsystems, components and clear actions, each working on a limited information necessary to perform that task.
You may sometimes even go crazy and duplicate the logic if it's no more than 2 places that uses that exact piece. The beauty is that while doing that, you are prototyping and have a real working solution, and if you find some time for a refactoring session, you will have exact places which you could improve. After all, code is easily refactorable, it's not like an architectural change, so may as well delay that.
Even worse, building abstractions just for tests. Which leads us to...
Who needs tests anyway?
Lazy programming is all about being lazy. That implies delaying writing the tests until some point in the future, generating technical debt, or less of it, as tests require maintenance too. I don't like TDD, it's very boring. So, here's the middle ground. For tests that require a lot of integrations or complex abstractions just test end-to-end manually. For code that is touching the core functionality of the software, is complex in logic and hard to reason about, such as custom clever data structures or algorithms, or even parsing and validation logic, write complete unit tests to cover most of the edge cases and you are good to go.
When is that exact point in the future for proper test coverage? One would be before refactoring code that proved to be working correctly. Another is after encountering a bug that was not caught when working on the implementation. The lack of tests may require some investment that you avoided earlier on, but that's fine if you want to avoid hitting the same bug again, which you probably do.
It serves quite well. Most of the code is straightforward with few conditions or loops anyway, and can be understood by inspection. Integration code is not complicated by just-for-tests abstractions, and the complex bits are covered well. For comparison, my current size of code for tests is about 10% of the total code size for that subsystem, meaning that the heavy and complex bits of the project are not that prevalent. I know, the project is not very sophisticated, and I should probably cover it slightly better with tests, but that's for later. I can always spare some time for tests when I'm tired and don't want to start a new feature, or during a day in a week allocated for addressing technical debt and polishing rough edges.
Part of me thinks that experiment will bite me in the future, but that will help me refine my software engineering approach. I do have worries about the correctness of my software too, but it's there in general, even with test coverage, as tests sometimes don't catch bugs that appear in production. Tests also act as a good form of documentation, so what about documentation?
Code is documentation
Everybody likes writing documentation. It's very precise and always reflects the state of the current system. So just write more of it.
Of course not! Being lazy means that you don't do what you don't like. Treat code as documentation, write it clearly enough with good variable names and constructs. Maybe throw a comment if you made some hack and describe why exactly you made it for a future reader, including yourself. The only things that require documentation are manual repetitive tasks that you cannot avoid at all, some conceptually tricky bits, or design docs. Design docs are extremely useful, just make sure they are updated after the implementation
For instance, in my project I wrote 4 design docs in total: infrastructure for production, security overview, application server design, and one about authentication flow and key management on the client side. Plus added a little documentation about managing postgres roles, which required some structure to it.
Again, the balance is about being practical and avoiding generating artifacts that you have to maintain later. Documenting every function is just a lot of work that has to be updated every time you touch that function, and I frequently forget to update the comments after changing the internals, but maybe it's just me?
Careful and precise where necessary
Yes, lazy does not mean sloppy. You need to recognize places which require attention. Examples include database schema design, infrastructure, core algorithms, applied cryptography and key management, performance critical paths. Basically stuff that affects the core correctness of the software, or something that is quite rigid and is hard to change in the future. These are also the pieces that benefit from a design, be it documented or just a solid mental model. The design is usually the hardest part, implementation is secondary in complexity.
How does laziness apply here? The laziness here is to avoid future work. If you mess up the rigid part just imagine how you would fix it on a running production system. Do you want to do all that work? I certainly don't, so I avoid that work by doing more work upfront, which is far easier as you can make breaking changes during a day, instead of aligning rollouts across multiple weeks, surgically applying changes one by one.
In short, I think it drills down to having some internalized judgement of when to avoid making initial investments. You can also describe laziness from the point of view of options theory. You pay costs now to exercise that choice in the future, and if the future aligns perfectly, it's a win. The early work on architectural decisions will determine the future of the project by large. However, pretty abstractions followed by slight design change may as well throw away all that work. It's a tricky question of whether you want to make upfront investments, and weighing the likelihood of that investment paying off.
I'm going back to being lazy.