I learned to code the hard way, but it was worth every minute. Unfortunately, not all of us who teach ourselves are able to pick up the same terminology and patterns used by others. In fact, I hadn't heard about "Design Patterns" until I first listened to a podcast about it a few years after I started studying. Not all of these just apply to self-taught coders either. I've had junior developers a year out of college look confused when I've mentioned a few of these.
What is a design pattern? Why should you care? And will it change how you code?
A design pattern is a simple or complex pattern that naturally evolves to solve some problem. I'll cover a couple here, but there are tons or resources on individual patterns, variations on these patterns and libraries and frameworks that help to achieve these patterns for almost any language if you give a quick search.
The Factory Pattern is one such pattern that you may have already used without knowing what it was. Factories are useful for creating objects where the consumer doesn't have to know much about the object it's creating. It's especially useful when you have need to generate an object or some other data structure, but that structure may change over time.
Maybe it's user creation for a website or a desktop client. The UI shouldn't need to declare every last detail of a user and it shouldn't have direct access to the database where the user is stored (for security). So what do we do? We make a method on our back end (server) that the front end (UI) can call to get a user created. The "Factory" in the back end can look up any preferences the UI needs to know about and grab extra details like the user's display name or avatar before sending the object back to the front end. And then the management software you use can also use this same factory to retrieve user details that need to be edited. Thanks to the factory, you can add methods to make the user an admin, moderator or guest all without having to change how much either UI needed to know about the user and how it's stored in the database.
The visitor pattern is less concerned about making low level updates easier and more concerned with not breaking legacy code. By making a special construct that only "visits" legacy code, you can extend it's functionality without altering the behavior of legacy code.
Say our website has new features for selling products that some users can opt into. It could be quite reckless to jam this new set of behaviors into our existing product, storefront, and user classes. But at the same time, rewriting all of that code or, even copying some of it could cause issues with our new code that were already solved in the old. The visitor class we create can use our old constructs (users, products, etc) but add new functional behaviors on top of them. It's not fool-proof, but it's safer for brittle legacy code and an option when changing the source code of what we are visiting is not an option.
Jargon is any special word or that only has meaning within a specific field. You likely came across some of it while learning. "Compile", "variable", and "function" are all jargon.
Despite anything you may have heard, copying code is OK much of the time. Copying code with little to no modifications is what we call "Copy-Pasta" (like Copy and Paste). Generally this is used with negative connotations and implies that the copied code was not thoroughly examined or adapted to it's new usage.
You may have heard someone tell you that your code or some method is slow because of all the "allocations". An allocation is when memory is set aside for use. This takes time and generally slows down the code. Zero-allocation code usually uses some tricks to pass the data around without allocating more memory addresses to it. This means better performance all around.
This can have different meanings depending on the context. Runtime can literally be the amount of time it took to run or more commonly the program that manages and runs your code. Kotlin and C# both have runtimes. C# uses a version of the .Net runtime called the CLR (Common Language Runtime) and Kotlin uses the JRE (Java Runtime Environment). The runtime is responsible for a number of things such as garbage collection or memory allocation.
Stack and Heap
These are rather low level terms that may not matter much to your early learning, but knowing what they are will help as you grow. Stack is the memory that the processor has at hand and is generally the fastest place to put things. It is much smaller than the "Heap" though. For larger objects, you generally put them in the Heap.
References, Pointers, and Values
These are different ways to describe a bit of data. A reference or a pointer is more like the name of a person or their home address than the person themselves. A value can be considered the actual person. So if I gave you Bob's address, you could go tell him what you needed, but I can also go his house and tell him what I need. If Bob wanted to give you his house, he'd have to actually make a new house since he is still living in his. Because of this, it's generally better for performance to not make Bob build a new house every time someone wants to look at his living room and instead just give them a pointer or a reference.
"Keep It Simple Stupid" - We all have a tendency to over engineer things. Want to a sandwich? Balance on one foot, flatten a spoon into a knife, juggle the condiments...etc. KISS is one of the most important philosophies you will ever follow. Simple code is easier to read, easier to maintain, and less likely to break. Consider trying to make that sandwich by adding bread and the filings with no ceremony or hoop jumping. In more realistic terms, this is avoiding the drive to "do something interesting" or bleed as much performance as possible out of you code when you don't need to. That function may demonstrate the neatest quirk, but if it's difficult to understand, you or whoever has to read it next will hate you.
SOLID is an acronym for a tried and true set of philosophies to guide your code:
Single Responsibility Principle Each piece of code should do one thing and do it well. A class/function for reading a file should not also attempt to parse it out and display it. Split the functionality up and use higher level functions/classes to "handle the file". And this also means it's OK to have higher levels of responsibility. A Manager runs a store, but the employees handle the sale and the register handles the transaction.
Open-Closed Principle Code should be open for extension but closed for modification. Extending existing code with new fields and functions is much safer than changing existing fields and functions. And the more higher level classes/functions that depend on it, the less it should be changed. If you made a Pizza class, it's OK to extend it with more toppings, but if you change the pepperoni to salami, the consumers of that pizza are still going to expect pepperoni. Much like with pizza however, this is not always safe. In code, if a switch is expecting 1 of 3 values and errors out if it's not one of those, adding a fourth possible value could cause unexpected errors.
Liskov Substitution Principle The idea here is that if you request a specific type, you need to be able to handle any derived type without knowing what the type is. Example, a Pizza Place delivery person should be able to deliver any kind of Pizza Place product without knowing exactly what he is delivering. Specialty pizza, wings, soda, etc. All the delivery person needs to know is that it has or has not been paid for and where it needs to go. If I'm calling a method to get an integer, I need to be able to handle that integer without knowing whether or not it was in a trusted./expected range. This means having logic to handle unexpected numbers, but not having to worry about decimals and other fractions.
Interface Segregation Principle If you have similar functionalities and interfaces shared by a number of consumers, you should split them up into individual interfaces/behaviors. At the Pizza Delivery Place, Vegetarian and Meat pizzas both share toppings, crust and sauce interfaces. Grouping these into a single pizza interface would work for calzones as well. But what about those wings? Now we would have to re-implement the interface for sauce to leave out the crust and toppings interface. But now we also sell Cookies that have toppings but no sauce. These all have to be menu items to be sold. So by separating these out, a Cookie just sets the same toppings used by the pizza, but knows that it can only have chocolate chips, sprinkles, or bacon (because the world demands bacon) and a pizza knows it can also have chocolate chips, but not with a marinara sauce.
Dependency Inversion Principle Your code should always depend on abstractions instead of the concrete implementations. This is especially important when you need to switch out something. If I had a class that was responsible for storing objects in a database, I should be able to switch it out with a class that stores them on disk or in memory without the rest of my code caring. This can be even more important when switching between databases. If my User object is sanitizing the name field for SQL Injections, it may pull back that same sanitized name instead of the correct name on the next go around. And worse, sanitizing it for one database may leave out necessary changes in another.
Rereading these once a year helps me to internalize some of the concerns and catch myself anytime I would violate them. I can never remember the actual terms for each letter, but knowing what they are can keep you up to speed if someone mentions them
Don't Repeat Yourself. When someone wants to DRY their code, they didn't just spill that caffeinated nectar from the gods on their laptop. DRY is an acronym for making sure you reuse code whenever possible. Got code for notifying users about an update? Extract a function/class to notify users of anything instead of rewriting that bit every time you need to send a notification. This is most important when it comes time to fix bugs. Ever played whack-a-mole? Try getting bug reports about missing notifications or bad format in notifications the day after you just fixed it. And again and again. Reusing the code makes it easier to fix it in one place then move on.
Much like that saucy pasta, some code strings down into the depths only to spiral back up again. This means anytime you have a error message saying where the error came from, it points to a few hundred lines of possible failure points. This is generally a violation of the Single Responsibility Principle and can be very difficult to clean up,
Lift and Shift
Lift and shift is an alteration to existing code that should be low risk. This could be moving behaviors into a library so it can be shared with another application or moving the code to a newer version of the framework/runtime.
Not Everyone Knows X
Sometimes those who taught themselves do not understand how difficult some concepts are because they hit them earlier than a structured system would teach them. Multi-threading and parallel programming came easier to me because that's what I started with. In fact working in synchronous only code was a bit of a struggle at first because long running IO had to be waited on and optimized differently. But at the same time, a lot of people talk about it being easier. Package management is the same way. I've talked to developers that don't know how npm or nuget package resolution works because they were taught to ignore that and focus on the "Fundamentals" that I had to learn later. Always approach subjects assuming everyone knows about it but don't downplay it no matter how simple it is.
Unit, Integration, E2E and Black box Testing
Unit tests are tests that target the smallest testable portion of the code. They offer the least immediate protection overall and take the most time, but ensure the very foundations of the code work. They often require faking out any dependencies and as such can be used to test scenarios that are not possible with current code but could be in the future.
Integration tests combine these units and offers the greatest amount of immediate protection for the least effort. This covers all possible interactions of units and does little to no faking of interfaces and dependencies.
End to End or E2E tests focus on flows from the top of the program and offer a lot of protection for a little more work than integration tests. This can overlap a lot with integration test cases and black box test cases.
Black box is interesting as it almost requires the tests to have no insight into the implementations of the code itself. These are generally the same as E2E tests but ignore the limits and restrictions of the code. This is most useful for scenarios where you really want to know how a proprietary piece of software (such as Chrome or Firefox) interacts with your software. By ignoring implementation and focusing on how the user will approach the software, you can catch some interesting or rare issues that are not likely to occur in tightly controlled E2E test cases. This also means they can be much flakier than E2E tests. This can be compared to the type of testing that is often done by manual QA testers.
Not Everyone Tests
This one came as a bit of a shock to me. After reading the importance of testing on many sites and in countless books, I found almost immediately that testing can be seen as a waste of time by some. Automated testing is even less appreciated to some as they can be seen as a drag on resources instead of adding value. This includes security testing. While it's not deadly to avoid testing, it certainly makes adherence to best practices that much more important. That said testing does slow down production and has an impact on the bottom line (disregarding the cost of bugs).
It Doesn't Need to Be Optimized Yet
Premature Optimization is the source for a lot of headache. Learning those optimal methods til they become second nature is great, but recognizing when you should avoid them is better. If a user facing program reacts in under 150ms, you probably don't need to spend hours shaving that down. If a high-traffic method on a server is taking over a second per call, you could probably save money and get better throughput with some tweaks, tricks and dirty hacks. Also remember, that many languages optimize themselves to a degree when they run. If you just really enjoy optimization and nothing else needs your attention, still give it a second though if it's frequently modified code. Optimized code can be harder to read, more brittle and sometimes nullified by changes down the line.
If you stop learning now, no matter how long you code, you will stay at the same level. I have worked with people with a year of experience that were well qualified to be called an Intermediate Developer and I have seen those 2-3 years in who are still Junior. At the same time 15 years of experience does not a Senior Developer make. Many positions will give a length of experiences as a reference for how long you need to have worked in a field or which degree you have to have. Give them enough work to prove you know what you are talking about and many will waive those requirements. And some will not. Find a place that values you that you taught yourself and you will be far happier than you could be somewhere that has a hard degree requirement.
You Don't Need X
By the headlines in the news, we should all know that we need to be concerned with security. This includes internalizing safe coding practices and accepting when others point out potential security flaws in our code. We also need to learn about any features our chose stack offers to increase security.
I could spend a lot more time writing things I learned early on and things I wish I had known earlier but I've written a good bit. If anyone has suggestions, I'll add em. If anyone just feels like they need to learn more: listen to podcasts, read blogs, and work on side projects. That will keep you going.
Did you find this article valuable?
Support Curtis Carter by becoming a sponsor. Any amount is appreciated!