On Developing Software
As I depart one position, and start a new one, I realise I still have so much I want to share with the youthful team I'm leaving. This is a bit long, which probably won't surprise anyone I've ever worked with. Nearly 21 years of experience (more if you count co-op jobs) to sum up. Some of this will be general, most of this will be unique to software development. None of it will be proprietary. And much, I'm sure, will be missing.
Play.
This is first for a reason. Don't be afraid to try things. Play with the code. This is software development, not bomb disposal - the worst that can happen if you do something wrong is that you corrupt your local code. Just undo your changes and move on.
Sometimes it's a good idea to break things on purpose. (Don't check that in, of course!) See what happens. Some of the biggest discoveries have started with, "oh, that's interesting" - many personal discoveries start that way as well. Do you get a compile error? A runtime exception? What does it say? When you see it again in ten months, you may recognise the issue and be able to resolve it. It's most important to ask "why" - as in, why did you get the answer you got? What is the root of that issue? Perhaps you expected to get an error but didn't - understanding why will be key to growth.
When you've played with enough things in enough ways, you'll gain some insight as to how things work. The relationships, whether between build and linking or between components of the (legacy) software you're tasked to work on, will become clearer. And problems will become familiar. Sometimes you'll have someone with more experience to lean on for debugging weird problems, but sometimes that more-experienced person needs to be you. Don't be afraid to play and gain that experience.
Not everything will work. Not everything can work. But everything can teach you something. Especially the failures.
Developers who don't have the innate curiosity to play with code and make discoveries seem to have a shorter lifespan in this industry, or a shallower career curve. Embrace your natural curiosity!
Speak up.
I've said this to almost every junior developer I've worked with, with possibly a few exceptions: if you're in this industry, you weren't hired for your looks, but your brains. Don't just sit there looking nice, the only way that your team can benefit from your brain is if you express yourself. Speak up. Ask questions, add input. The worst that can happen is that you educate yourself, and can understand the current topic, or the big picture, better, but I've lost track of how many times a question caused me to think about things differently, and we end up with a solution that is better than any one person could have come up with on their own. This is why even junior developers speaking up is so critical. Your contribution can make major improvements to the product that can have reverberations for years, not to mention improve your own visibility which helps your career.
Tools are the foundation.
Learn your tools. Learn your environment. Understand how your code goes from text to user. Don't settle for "it's a black box." If you're doing C/C++, know how your code goes from .c/.C/whatever to object file to library or executable, know how your code gets loaded (if a library). If you're doing Java, know how your code goes from .java to class file to jar file, know how class files are found and loaded. Once you know how the flow from code to user works, you'll be in a better position to deal with the weirdness that happens when the black box misbehaves.
Learn, too, how the provided libraries work. You should know how something like strcpy is implemented. You should never implement it yourself, but you should know how it works so you can understand why you crashed it passing in NULL, or why it takes so long to run, and thereby know whether your proposed fix is really a fix or just a crapshoot. And here I use strcpy as an example - the same holds for most POSIX functions or the std::* namespace in C++.
Software development may be part art, part science. But no part of software is magic. Problems that go away on their own will generally come back on their own.
Big Picture.
Always look at the big picture. Know how your tasks, whether self-assigned or assigned by others, fit in to solve the user's problems. Sometimes it's a small piece, sometimes it's a big piece, but if you don't really understand the user story associated with the task at hand, there's a good chance you're going to miss something. This could be an edge case or it could be the core functionality. And by "user" I don't always mean "end-user". Your task may be to reduce technical debt, and the user would be you, in six months.
Understanding the big picture helps prioritise your work. Any time I was asked to prioritise work, knowing who would be helped by a fix or feature as well as how many would always be a major factor.
Understanding the big picture also helps design work and refactoring. Knowing how business needs in this chunk of code will change can inform how you design your code to reduce the number of assumptions that will become invalid over time, or to maximise flexibility that will allow changes to be made more easily over time. It's a balancing act between YAGNI and, well, you are going to need it. Don't write function you don't need, but having the hooks for it means your code is more likely to have looser coupling and stronger cohesion. And this will keep the future psychopaths at bay.
Maintenance is life.
Writing brand new code is always way more fun than maintaining old, legacy code. Unfortunately, any code that is worth anything will spend more time in production than in development. This means bug reports and feature requests. Learning to write code that is easy to debug seems like an art, but it really isn't. This goes back to the previous points: play with stuff to learn how things operate, this will tell you a lot about how to debug code, and know the big picture to know what your edge cases look like.
Know how your code will be used. Find a way to get stack traces and relevant data at the point of failure. This may be as simple as writing out a trace log - function entry and exit, arguments, and key decision points. It may be catching exceptions and only logging those. Trace logs are better when possible just because sometimes the failure is a logic error going down the wrong path, and there's no exception thrown.
Know how your code will adapt. The one thing that is constant is change. Or something like that. It's inevitable that there will be changes to code in maintenance. Whether that's new features or bug fixes, it's going to happen. Copy-and-paste means that you have to fix it in multiple places.
Find a design model that resolves this, and apply it. Better yet, find multiple models and use the best one (or more than one!) for this job. My favourite has been a data-driven design: put data in one place, and then create a loop to iterate through it, using the data's attributes and such to determine exactly how to perform the loop. The data can still be in your source file, but the loop cannot use the "name" of the data except for input and output (such as tracing that you're working on the data named "foo"). Never have code that looks at the name to do things, but create attributes or metadata that indicate whether to do that item or not. If your code says 'if (object.name == "this" || object.name == "that" || ...)', you're doing it wrong. Instead, have metadata on the object that indicates whether this piece is required or not. This allows the objects to change according to business needs - both its name and its behaviours - without making a mockery of the code. The data can live close to the loop, or can live in separate source file(s), but should not be inside the loop.
Maintenance is Debugging.
Get familiar with the debugger. Whatever that is for the language/stack you're working with. It is your friend. Debug software that is working perfectly if you have to, just to get familiar with this tool.
Reproduce your problem, when possible, under the debugger. If you can reduce your problem by using fake scaffolding (code that pretends to be the rest of your program, at least under the error scenario, without all the excess side effects), even better.
Even more importantly, design your software to be scaffolded. That is, design and write your code such that it can stand on its own easily. Write your frameworks such that they can fake aspects of themselves for test purposes. Enable write-compile-test cycles without your full product on display. If this sounds like microservices, it may be because those are good ideas, but this is an order of magnitude lower-level. Try to have your microservice be made up of nanoservices. For example, a task-oriented application may be made up of many small individual task objects, with some sort of task manager object coordinating. Add to that manager the ability to run individual tasks without preceding or following tasks, allowing you to run that one task under the debugger. Add the ability to stop at or skip individual tasks. And then be able to run an individual task in a debugger. And standalone in unit tests. (If it sounds like I'm skipping over unit tests, I am - but only because the case for unit tests is so well made in so many other places that I feel it redundant. If it's not redundant to you, please search on the topic.)
You will need this not only during development, but even moreso during maintenance. Knowing how to add new functions means testing those new functions. However, if your application is a behemoth, you will end up spending more time testing the rest of the application than your one piece. This extra upfront cost will make maintenance much cheaper, and designing for it up front is similarly relatively cheap - it pays for itself very quickly.
The bottom line is to always keep maintenance in mind. It's always cheaper to do so during initial design than later, whether that's brand new code or just a fix / feature enhancement in existing code. Keep an eye on "how can I troubleshoot this later?" any time you touch the code.
Scripting is productivity.
Learn at least one scripting language. Preferably two or three. Never do anything more than twice. The first time is to figure out how to do it, and the second is to teach the computer how to do it. Scripting languages, languages that are geared towards rapid development, such as unix shell, perl, python, ruby, are great for hammering out something quickly. (Some of these are also good for full-fledge programming, so don't dismiss them for "real work" just because I'm calling them "scripting languages" here.)
Spending a couple of hours creating scripts that you'll use hundreds of times and save you 5 minutes each are worth it in the time savings alone, but can also save your sanity. Just by reducing keystrokes and time spent on mundane, repetitive chores, you lose less focus on the real work you're trying to accomplish, which can also boost productivity.
Work is not life.
Work is what you do to pay for the life you want (or at least can afford). It is not your life. Do the best you can with work, but don't forget to live, too. I don't know how you define "the life you want" and I'm not here to tell you how to define it. I only am trying to help you earn the money you need to pay for it.
Great stuff!
A good read with a lot of good points. I think I'll share this with my son's CS class. Good luck to you on your new digs and thanks for all of the great chats over the years.