The honing of a software engineer
Freddy Guime
My role is not only about architecture, but also about purposeful leadership, be this mentoring, coaching, consensus-building or generating new perspectives, all while nurturing great teams to produce the most impact
Sometimes it is hard to figure out what makes a good engineer. There are the basic foundations of the craft like knowing your programming language, algorithms and being comfortable learning new things. There are also skills that are not technical and still necessary, like good communication, estimation, project management and leadership.?
Today, I want to explore what makes a great engineer (from the technological perspective)
If we boil down to one statement. I would say that the best software engineers focus relentlessly in one goal
"Create code that's hard to make mistakes in"
That's it! That's the efficiency loop that keeps driving good engineers to be better. The outcome of such statement is code that is simple, and maintainable. It will be code that doesn't look complex, but achieves a lot. It'll be decisions that may look boring, but at the end of the day it represent the best that, as software engineers we can create.
From this ideal then spans fundamental behaviors that drive how we code.
You should be comfortable refactoring, even your own code, multiple times.
This comes with experience, but a truth in software engineering is that you don't create code that's "perfect" from the first commit. Because unless you are implementing very tried-and-true things (say some kind of sorting algorithm, which btw, you shouldn't) most of software engineering is around mapping business, and real-life concepts into code. This by itself is an art, and we might not know what the best representation, within our business context, is.?
A confession I make is that when I write code, I tend to go through many revisions, sometimes altering anything from the object hierarchy to (gasp) libraries or languages. This is because I learned the truth of our craft that is part art and part science. Just as a painter keeps brushing the same parts, mixing colors and creating shapes, we engineers do the same, trying to bring clarity to the picture of a system that is yet only in our minds.?
This also goes into the next corollary
It's ok to see code you wrote and want to do improvements
I see that some engineers feel guilt that they think the code they wrote a year, or six months ago, or even last week, could be done better. I think that if you see that code you write can be improved is actually a cause for celebration! I would be extremely worried if I look at code that I wrote a year ago and think that it's as perfect as I wrote it that day. So let's breakdown why the code you have written feels that it can be improved and why you shouldn't feel guilty about it.
You let the code "rest" (both in the project and in your head)
This is common, after letting the code be used within your service / program, you can start seeing what is working great vs what is not (usually you may get a lot of questions about a particular part of the code, or you are catching certain bugs in other contributor's PR). It's ok to write some piece of code and after deploying figuring out there were flaws, or room for improvement. Just jump in again a refine that code. Take that live experience of having the code running and use it to make what you wrote, better. There is no shame on this process.
Requirements are always changing.
The constrains we have a year ago are not the same constrains that we have today. The product team asked for new features, the way we use the service we created has changed. The technology we rely on has been migrated or retired. All of these things affect how the code looks like. So sometimes the reason we see that we coded something sub-optimally may just be that "it was the best code for the time it was created", but as of today "prior assumptions have changed". And that, also means that the code should change as well (PSA. you should change that code)
The language you are working on is changing
Programming languages are not static things, they keep getting better. The libraries that you have programmed against may have new features, or conciseness. Java introduced var in 11, switch expressions in 17, and now virtual threads in 21. Each of these (in Java and other languages), create an opportunity to further the ideal we keep chasing of "code that's hard to make mistakes in". Sometimes there are new frameworks (Reactive, Streams) that (may or may not), help simplify code that you wrote. So sometimes the drive to refactor code is that you now know have the ability to use something that's easier, cleaner, and less error-prone.?
You are learning how to be better.
This is possibly the best indicator that you are growing as an engineer. In a year (or six months, or even a week), you may have been exposed how to do things a little better. It could be a new way of refactoring, or maybe after looking at your code for a little your brain decided on a better way to simplify, or hide complexity. It's ok to think that code that you wrote can be improved. It's actually desirable, it means the next time you are in that part of the codebase, you can go ahead, and make it just a little bit better.
The job of the engineer is beyond writing the "ticket"
Sometimes we think the "speed" of an engineer is the most valuable asset (and what we are measured in). To be fair, there is some speed of delivery (we should, after all, deliver value), but I would rather have an engineer deliver something in a week, that is less error-prone, and has been refactored out to support the feature, than the engineer that managed to finish the ticket in a day. The reason is that the week spent on refactoring is way less expensive than the outage incident we can have in the future due to bugs, and the time spend creating new features due to the difficulty of understanding the codebase.?
To make this more concrete, assume that there's a ticket to "log extra information" about a particular event. The ticket specifies to do it only when particular conditions are met. For most engineers this might be one hour's worth of real work. Just find where it needs to be, and put it in there.?
Assume now though, that this is already in a larger, already deep and nested "if" condition
领英推荐
if (request.isBrand()) {
? ? if (request.forUpsells()) {
? ? ? ? if (request.forCaching()) {
? ? ? ? // do something a.1
? ? ? ? } else {
? ? ? ? // do something a.2
? ? ? ? }
? ? } else {
? ? ? ? // do something b
? ? }
} else {
? ? if (request.forUpsells()) {
? ? ? ? // do something c
? ? } else {
? ? ? ? // do something d
? ? }
}
After the feature the code would look like
if (request.isBrand()) {
? ? if (request.forUpsells()) {
? ? ? ? if (request.forCaching()) {
? ? ? ? // do something a.1
? ? ? ? ? ? log.forSpecific(request.loggingValue, "caching")
? ? ? ? } else {
? ? ? ? ? ? log.forSpecific(request.loggingValue, "caching")
? ? ? ? // do something a.2
? ? ? ? }
? ? } else {
? ? ? ? log.forSpecific(request.loggingValue, "upsells")
? ? ? ? // do something b
? ? }
} else {
? ? if (request.forUpsells()) {
? ? ? ? log.forSpecific(request.loggingValue, "nonbrandupsells")
? ? ? ? // do something c
? ? } else {
? ? ? ? log.forSpecific(request.loggingValue, "nonbrand")
? ? ? ? // do something d
? ? }
}
This was trivial to do, but the code itself is now more complex. If for any reason you need to change what's being logged, or someone adds a new if/else statement, they need to take care of copying the logging from the other branches as well.?
You can of course refactor the logging onto a method, but you still have the complexity of "not forgetting" a condition.?
For this contrite example we could do something like.
RequestType requestType = request.getType();
log.forSpecific(request.loggingValue, requestType)
switch (requestType) {
? ? case BRAND_UPSELL_CACHE:
? ? ? ? // something a.1
? ? case BRAND_UPSELL:
? ? ? ? // something a.2
? ? case BRAND:
? ? ? ? // something b
? ? case META_UPSELL:
? ? ? ? // something c
? ? case META_BRAND:
? ? ? ? // something d
}
With the caveat of being a contrite example, the flow now becomes much easier to understand, and the risk of missing or introducing an error (or forgetting to log a case) is now removed (We always log at the beginning with the actual type of request). If someone adds a new request type, they will easily follow where and how to add it.?
This would've taken more time to implement than what the original feature asked for, but it highlights what we are really being asked to do every time we implement a new ticket. It's not about just doing the "work" of the ticket, but also refactoring the code to both simplify what the ticket is asking, and reducing errors when someone needs to work on "top" of the ticket we built.
This is what it means to work towards "code that makes it hard to make a mistake on". The work of a ticket is not just "doing the feature", but "how can I make sure that for those engineers assigned to maintain this after me, won't make a mistake related to that feature". That's it, that's the job. So don't just write your ticket, but refactor as you go along.
There are no boundaries when it comes to Code
There is this unspoken contract that says "I don't want to touch that code, I didn't write it". Some of it is because we may have notions that others would get angry if we touch their code. Sometimes it could have been that's been policy from other companies we have worked on. Some of it is being afraid "The code was written by a very senior person, the probably wrote it perfectly" (From the above sections, we should know that's not the case).?
As engineers we need to not be afraid of refactoring and touching other people's code. We already know that code is "fluid", and that fluidity may take us to refactor beyond our current scope. That's perfectly fine and expected. If anything, other people would welcome the fact that you are helping not only your code but their code be better, clearer, less error-prone. If anything, let it come into the PR, most likely is that you'll get a +1, or LGTM!
A caveat. Code / PR is not a proxy for design/framework discussions There are more nuance when it comes to larger architectural decisions that are not clear-cut. For example, Spring vs Quarkus, or Reactive vs Imperative. If you are refactoring code because you want to bring in a new framework or a significant different way of doing things, then it must have been talked with, and agreed with the team. Saying there are no boundaries in code doesn't mean we can just imprint what we think is "best". The practical reason for going with "good enough" is that there are many ways we can achieve success from a software perspective. One solution, framework, library may be marginally better than what's used, but unimportant as measured by the outcome. So if the team decided in Java for their project, and you are bringing Kotlin because you think is better, you really need to get the team onboard before changing code and submitting your PR. If not, you are creating work for everyone for something that was already decided (or something that has not been adopted).
Don't be intimidated for what others call "great code"?
We all start somewhere, and to be honest, great code is simple code. This is code we can all write. Code that is "great" is not code that's complex, or have multiple classes, or does tons in two lines of code, or is "super fast". Great code is one that is so simple, you can understand it just by looking at it.?
There used to be a misconception from engineers that great code has to be large, and convoluted, or so abstracted that everything is just a templated map or a list. None of that really matters when it comes to projects that have a team. The latent problem is that we have been conditioned for the longest time that good code is complex code, that we lost the truth that engineers are part of a team. I would always pick an engineer that writes simpler, but understandable code, than an engineer that can spew fast, compact code that is hard to read. I'll always pick simpler, because that's the end of the game, and worth repeating. We want to write "Code that's hard to make mistakes in"
We also need to be open to learning. There are concepts and tools that help you write simpler code, like cyclomatic complexity, Law of Demeter, Single Responsibility (sometimes this is kind-of hard to understand correctly, so we need tons of examples). There are concepts (Dependency Injection) and anti-patterns to inform (anemic domain model). There are ways to organize code to break down complex problems, like design patterns (adapter, decorator, factory). We have now AI agents that can work as our "on demand senior", and ask for example "how can I simplify this code?". It may or may not give you a great answer, but is something that you can start getting a feel for. There are conferences (like DevNexus, JConf, JavaLand), tons of online resources (Venkat Subramaniam, Josh Long) on how to write effective, simpler code. There are local user groups (I help run the Seattle Java User Group). These are all resources that will teach you great tools for writing simpler code. Even so, as tools they have their particular use where they excel. Don't throw all tools at all the code. Sometimes we don't really ?need a decorator pattern for every class.
In sum.
Being an "good" software engineer means understanding that we are working on a craft. That code is not "set in stone", and that it can change. That we work in a team together creating business value, that we need to be open to learn and to contribute to any part of the code, and that the most important quality of the code we produce is to be "Code that's hard to make mistakes in".
Hope to see y'all growing in your career and creating amazing code!
Senior Software Engineer | Experienced Full Stack Developer utilizing React, Angular, Vue and Blazor frameworks | Passionate Problem Solver
8 个月One thing I notice a lot of great engineers miss, is documenting their work. They write great code that is robust, but no one else knows what it does. Not just code comments, but documentation for systems explaining their use is also needed to be a good engineer.
Java Champion, JUG Leader, Sr. Principal Engineer
11 个月Freddy Guime - this post is spot on! Hope you continue sharing such awesome perspectives!
ML Scientist | Writer
11 个月Simple code is something to aspire to indeed. It makes everyone's life easier!
Android Developer
11 个月Love to see you advocating for this perspective, and glad to attest that this is exactly the advice you gave me so early on in my programming career, which I'm super grateful for!
Head of Java in Education
11 个月Love this perspective, Freddy. It is well reasoned and clearly written. And it calls out important stuff that we often don’t say out loud. I’m going to share this with a few early-in and new-to career devs I’ve been talking with lately. Thank you for this!