OK. So here you go. Here's my magnum opus.
Not really but here is a little info based on what I have found works and what I have found doesn't with this whole programming thing.
In general these are the rules you want to follow (according to me lol):
-
- Just say NO to complexity.
-
- Abstraction is good actually.
-
- DRY but ONLY after repeating yourself.
-
- YAGNI when you are pretty sure you will.
-
- KISS and why it matters to teams primarily.
Just say NO to complexity.
Complexity is the evil demon of programming. When I say complexity, I am exclusively speaking about those things in a codebase that you scrunch your face at and have to think really hard about. There are parallels between this rule and rule #5 so read until that point if you want my full thoughts on this. But in general, anytime you have to traverse endless inheritance structures, or indirection for the sake of indirection, you are encountering complexity. WE HATE COMPLEXITY. I want to read your code from top to bottom and know EXACTLY what is happening. I think this fight against complexity is the reason some folks will throw everything into one file and barely abstract anything at all. They choose to fight complexity by putting everything in one place, and using only built-in language features or low-level libraries pre-defined abstractions. I think the reason they do this, is because the abstractions in standard, or well-known third party libraries are actually good. They hide the details we actually don't care about, and let us work with reality but in a slightly nicer way. But, for some reason, these people refuse to write their own abstractions (BAD).
Then on the other side, there are people who OVER-ABSTRACT. These people are actually more common than the former. These are the kinds of people who follow design patterns, or build abstract DogFactoryBuilders that inherit from the base AnimalBuilder class that inherits from the EntityBuilder class, etc. etc. These are the cargo-culters that those who care about programming hate. This is abstraction without a need. They design the large system without first trying to get the small system to work. For this reason, there are about 2-3 levels of abstraction THAT DON'T NEED TO BE THERE AT ALL. And, for the levels that remain, they are BAD because they had no clue what they were abstracting in the first place.
Abstraction is good actually
There are two uses of abstraction that I think actually benefit programmers.
- Abstractions where things might change, or you are not yet informed enough to make a decision.
- Abstractions where things are so confusing, that you need to do it to cope w/ the ACTUAL complexity of the problem domain.
If you can come up with others, please let me know, but these are the only two REAL use-cases I have seen that don't demolish codebases.
For the first, consider the fact that humans are dumb, fallible creatures. Any decision we make today is ass compared to the decisions we can make in the future. Future me has more information, has more experience, and in general is a better guy in every way. I want FUTURE ME to make the actual implementation decisions for my projects. As such, when faced with a hard decision I CHOOSE TO NOT DECIDE AT ALL! I say, let future me decide. To do this, we create a layer of indirection right at this point in the codebase and we program off of this interface. This is where people have come up with ideas such as domain driven design or hexagonal architectures or whatever. They say, the most important thing to get right is the Domain layer (i.e. the part of the code that actually solves the problem). I vibe with this, don't take their advice as dogma, but most of their ideas are pretty good. Here's a concrete example for you: I needed users to fill out a form and to store that info in a database after some operations were performed on it. I don't know jack shit about optimal database design. I mean, I can learn, but I DELEGATE THIS PROBLEM TO FUTURE ME by creating an interface for what I expect to receive from the DB and coding off of that. This way, I program to the interface, and when future me feels like getting around to this task, he can just conform to the interface and THE WHOLE SYSTEM WORKS. In the mean time, feel free to write some dog-shit code that stores everything to a json file or something.
For the second, some problems are actually tough. Their solutions have many moving parts and they are confusing. In these cases, you want to abstract away anything that is hard to understand and build up the solution at different conceptual layers of the problem. When you talk to the people who solve the problem you are automating away, they speak using specific terms (read: jargon). The jargon isn't ALL for nothing, some of it is just their way of referring to a specific situation or scenario with ONE WORD. In your programs, you want to program at their level. I should be able to show them your code, they look at it and go, "oh, yeah, that is pretty much exactly what we do." They shouldn't have to know anything about coding to be able to read your solution to their problem. What I mean by all of this, is some problems are hard. You break up hard problem into tiny, orthogonal(ish) pieces of the problem. You then combine these pieces together a layer up in your abstraction hierarchy. Each layer should only depend on the previous layer. This means at the highest level of abstraction, you should be able to reconfigure your entire system with one function call. Wild stuff. But people do this prematurely and get bit in the ass.
DRY but ONLY after repeating yourself
This one hits the newbies hard. They hear DRY, and think their knowledge of it makes them superior to others. PLEASE REPEAT YOURSELF AT LEAST A COUPLE OF TIMES. If you don't do this, you will GUESS at what to abstract away, and it WILL be a shitty guess (unless you are experienced -- I am not). By repeating yourself a few times you get two things:
- actual pain from having to repeat yourself
- multiple REAL examples of what you might need to abstract away
These two things are crucial. The pain makes you feel the need. If there is no pain, there should be no abstraction. Multiple real examples means you don't have to guess when building your abstraction. You can just look at what ACTUALLY changes between the two or more implementations, and build those points of change into your functions or whatever.
YAGNI when you are pretty sure you will
You aren't gonna need it is probably true. But in this specific instance I think I WILL need it. What to do what to do. I don't wanna break the rule of those who came before me. How do I proceed? This is the only rule where I think you should follow the dogma. You literally will not need it. And when you DO need it, you can just.... add it? I have found that if you don't have an overly-abstracted mess of a project, adding things is actually EASY. My genuine recommendation, is to program in such a way that adding the thing you are anticipating will be easy without actually adding that thing in. If I was working on a project that MIGHT need to handle internationalization, I wouldn't necessarily try to pull in a full internationalization library, and translate it into Spanish, German, and Mandarin. But I might code up a little partial implementation where I can easily replace strings across the application. THAT'S IT. This DOESN'T pull too much time away, it PLANS for the future, but I don't OVER-IMPLEMENT this perfect i18n that no one was really asking for anyway.
So, idk, figure out what this little partial implementation is for you and do that instead of going gung-ho on a feature that doesn't really matter anyway.
KISS and why it matters to teams primarily
I don't usually work in teams. SOLO DEV FTW. But KISS isn't just useful for teams, it is also useful for future you. Remember I said that future you is smarter, better, and more handsome than you? Well, all of this is true but what future you lacks is context. As in, you currently writing your function have EVERYTHING LOADED INTO YOUR HEAD. Future you needs to make that journey again in the future. You want to make this journey as EASY AS POSSIBLE for future you. Anything complex, any magic strings, or variables named x,y, or z are CONFUSING. No, I don't know what you mean when you are multiplying x by ITERS by 883920 Quib from the past! If you have some fucking insane functional composition of functors and turbo-lambdas and what have you, this is ALSO NOT GOOD! I actually like functional programming, but the problem with it is that no one else knows what the hell you are trying to do. If you do have stuff in your codebase that is either COMPLICATED, or CONFUSING to normies, you should write a COMMENT about what it does. I should be able to read the comment, take a quick glance at the code, and be like, huh, I guess that's what this function does. There is no excuse for this on confusing code. If you can write the code, you can describe what it does.
The reason I think this principle is most important for teams is this: on a team, there is a gradient of junior -> senior -> gods. At any point in time, a junior should be to take a peek at what the seniors are doing, and vice versa. If the only complexity in your codebase are things that actually need to be complex (and those complexities are documented w/ comments) then this can HAPPEN. You can get team-wide collaboration. The juniors can read the comments to learn, and the seniors can "ramp up" their internal context quickly to get to work. The Gods can just keep doing whatever it is they do.
umm. i kind of lost motivation to finish this, so there you have it! This was "A Quibble's Guide to Programming" - by Quibloo.