To reach the above mentioned stage what did you do? And what kind of mental model helped in achieving this state?
Seek feedback on your code. Fresh eyes provide fresh perspective. You’ll learn a lot about yourself from others commenting on your code.
2. Read other code. Get good at reading other code. Develop curiosity around understanding how the code works. Every time you hear about something you don't know research it superficially and make a note to try to understand it in depth if it seems important
3. Hang out/Work with people that are good at this. Seriously. Sometimes a person that understands the subject, a practice, one specific area can ELI5 to you in under 10 minutes saving you literally weeks of struggling to understand. Once you figure out who these people are, watch what they are doing (watch the PRs) and actively seek out opportunities to work with them
4. Aim to continuously learn and grow. Be proud of your work but continue to push your limit. If you learn OR teach one thing per day you'll be in the the top 5% in no time (an by no time I mean a few years but it's still nothing compared to doing this all your life and not being good at it).
I had heard about tests for a long time, but they were always presented as tedious and dry. In my experience, that's completely inaccurate. Even as a solo programmer, tests are amazing. They let you stay in the "if it works, it's good enough" camp as long as you need. "Okay, it works. Write a test that proves that it works. Now write some more specific tests that prove it works in specific ways, under specific questions."
Having tests in place lets you start to experiment with your code in provable ways. The only other thing that comes close to the impact of testing is learning to properly profile a codebase. But if I had to choose one, I'd choose testing to focus on.
Yes, it is cheeky in a uniquely 80's way
Yes, it is an ancient lisp that nobody uses anymore, but that is just syntax. The composition and data structure lessons of that video series is timeless.
And yes chapter 7 (and maybe others) most likely requires a few rewatches... even for a proficient programmer.
An no, don't try to read the book instead. Start with the videos, so that you can get the Big Ideas instead of fiddling with Lisp code, that you will not use ever again...
* = https://www.youtube.com/playlist?list=PLE18841CABEA24090
The other one for me was when I started reading OSS codebases on Github. Meaning, if I needed to use a library, I would read the source first. That brought me a lot of confidence and allowed me to see how others put things together.
The closest modern example might be using "View source" and making your own complete web pages/sites. The benefit of this approach is that you don't get value in micro-optimization of a single function and have to balance effort over the whole.
What I do nowadays is lots of code reviews. For my own PRs, I review them myself first with the mindset of someone who starts with near zero context. During development, I'll prototype something that just works (git commit and push to branch), then revise some parts that may have bad expected performance or low coding standards. Difficulties in naming or describing the work in a PR description may mean that you don't understand the problem/solution well or decomposed it in arbitrary rather than meaningfully stable ways.
One thing that's not often mentioned is working on old/large/legacy codebases--in particular ones you've written. It really gives you humility and a long-term view of what was actually needed and what didn't age well (YAGNI).
This will put you above "good enough" into 'should be good enough for anyone' territory.
That said, IMHO one of the more important epiphanies comes with maintenance of a complex system against evolving requirements over time. Until you've done that, your designs will never be maintainable and your documentation will never be adequate, and a design-for-others / design-for-future-selves mentality is arguably the best definition of 'good enough'. Put everything in version control, and document your requirements, reasoning and failures.
If you are designing anything that is intended to last longer than 6 months, documentation is a critical part of the system. Meetings are great for communicating with people here and now, but only writing can communicate with people from the future. When you meet with your current colleagues, spare a thought for your future colleagues who haven't yet joined, and do them a favor by writing things down. Ability to use written communication is a major differentiator between junior and senior engineers. - @jl6
Another good epiphany is small is beautiful. Do one thing and do it right. Use collections of scripts instead of integrated monoliths. Don't be afraid to throw them away and rewrite them. Interfaces are far more important than code.
The cheapest, fastest and most reliable components of a system are those that aren't there. - Gordon Bell
Decrease fear of failure. Experiment constantly, fail early and often, remember and document those failures, and learn as much as possible in the process.
You can read more wisdom at https://github.com/globalcitizen/taoup
> what kind of mental model helped in achieving this state?
Something like stoicism. You are surfing a big ball of mud called "the industry" and there's not much you can do about it.
The bad thing is that there are no shortcuts to becoming a confident engineer. The good thing is that everyone has to go through this process.
So don’t feel intimidated, just focus on practice, find tech that really engages you vs tech that someone else thinks is good/complex.
Obviously, you should write down all of their generalized advice, and make sure that every poll request in the future incorporates it. Within six months you will probably be getting very few comments as you have 80/20 internalized their experience
Now I'm the one often giving feedback to others.
I haven't.
1. Write out requirements in a form following RFC guidelines: the keywords like MUST, CANNOT, etc. are very, very important because they can be broken out into a spec for a single codepath or function: when your spec says "it always has to do this, and it can never do that", you can make simplifying assumptions.
2. Skeleton code: Your first run at the problem should be to attempt to return answers statically: if "a", return 1, if "b", return 2, and so forth. Many practical engineering problems are effectively "lookup table code" with routine answers and do not need a solution more complex than this. This can help even if your spec implies that a non-linear solution with some kind of algorithm is called for, because it immediately gets you to a point where you are filling out enough of the spec to see if you need to revise requirements, and you can build tests in tandem with this process. Often a run at the problem fails not because you can't keep going, but because you realize that you can define it in a more simple way. You want to iterate towards the simple because the alternative is to heap up requirements that are more incidental in nature(e.g. a feature that exists because of two other features that produce edge cases when used together - a very, very common outcome in complex systems).
3. Algorithmic strategies. OK, you've filled out enough to know that you need an algorithm. What class of algorithm will it be? In the majority of cases it'll be one of these:
"I have an analytic problem using a standard math formula" (therefore you implement the formula)
"I have a sequence of symbols that needs to be mapped into a different sequence of symbols" (this describes most parsing problems)
"I have a data collection or an answer set resulting from a prior event that needs to be searched, ranked or ordered" (list processing, constraint solving, AI; if the data needs persistence, serialization formats, databases etc may come into play.)
Master application of just those strategies, and you've covered many domains of programming entirely. A lot of the literal aspects of coding appear in setting up and tearing down the environment needed to apply that core algorithm, and then in providing enough of a user interface to configure the environment(whether it's an application user or another developer interacting with the code later).
At scale, code is building on other code, but in the sense of "communicating with a ghost". You don't get to dictate how older systems and APIs that you're building on behave. Often you can abstract them, but that only means you're talking with your abstraction, which in due course turns into another ghost if it actually gets put into production.