A simple (but not easy) way to improve your code quality is to only call it done when you fully understand the code you have written; only merge it or open a pull request after you understand it.
Simple because it does not require specific knowledge of broad programming principles and patterns or other technical jargon, but not easy because it requires effort on your part to learn or make additional changes to the code.
Did you copy some code that works, but you don't know why? Are you changing an existing codebase? Take the time understand why it works. Maybe some parts can be changed or removed for your use case for more clarity or cleaner code.
Focusing on the parts of the code you do not understand creates a roadmap for your learning that can be applied immediately vs studying/learning broadly which provides value more over the long haul.
Time-box your effort to understand the code, if it is going to take too much time, encapsulate the hard to understand parts into private functions/classes as much as possible. The complicated bits can be hidden from consumers of the code.
I like to do a self-review of my code with a @github diff and make minor improvements before opening a pull request for review. This helps by zooming out to see the change as a whole and see what parts can be improved for understanding.
Naming things well is more important than any programming principles. Good naming builds understanding due to the shared natural language, without requiring any special technical knowledge.
Naming in programming is done with a natural language (e.g. English), so when both the author and readers know English, for example, that shared context enhances code understanding regardless of the technical details.
The benefits of good naming apply to everything in code that is not a keyword or symbol – variable names, function names, class names, packages/namespaces – so apply good naming broadly.
Programming principles are still important, but their power and understandability are enhanced by good naming.
And remember that you often become the reader of the code you have written – days, weeks, months, or even years later – so be kind to your future self and make the code easy for you and others to understand.
I really like the concept of coming to terms with an author as defined in the book How to Read a Book. In addition to books, this also applies to reading code, as in code reviews, refactoring or fixing code, and reading open source code.
You cannot truly understand a book or code until you have come to terms with the author. Words have ambiguous meanings, so you must identify the important words and also come to understand the meaning intended by the author.
Its risky to fix a bug or refactor code if you have not yet come to terms with the code and its author. You are likely to create new bugs due to misunderstanding the code you are changing.
With a book the author's intent is all that matters in coming to terms. The author may do a poor job at communicating their intent, but once you have come to terms you understand the information or knowledge they are conveying.
With code, since a computer will interpret it exactly one way, there are two aspects in coming to terms with code. You must come to terms with the author's intent, and you must also come to terms with what the code actually does.
It is possible that when executed by a computer, the code does something different than the author intended. The author of the code may have failed to come to terms with the language or library they are building their code upon.
Even when the executed code and the author's intent match, you must come to terms with both prior to making any changes. There may be implicit assumptions in the author's intent that are only clear when you come to terms with both.
This is why naming is so important when writing code. Naming conveys intent, and makes it easier for future readers of the code (including your future self) to come to terms with the code and the intent.
Understanding the domain of the code can help in coming to terms. This provides context and helps to narrow down the meaning of the terms being used within the code.
If you are having trouble understanding a piece of code, search for the parts that you do understand. Use the parts that you do understand as context for the parts still to be interpreted.