Alexander Pope stated in “An Essay on Criticism,” “To err is human…”. It was true then, and it’s definitely still true now. We tend to learn certain ways of accomplishing tasks even if those are wrong, inefficient, or irrational. In addition, we are often stubborn and lazy depending on our moods for the day. Eventually, we fall into a pattern of only using our procedural knowledge of tasks while completely forgetting the declarative knowledge that helped us learn and begin performing the procedures. That’s why we’ve put together this guide on how to overcome bad programming habits.
What does that have to do with software development and programming? Developers and programmers are human too! As such, we suffer from the same shortcomings that people all over the world do. In academia and in the office, we may pick up the bad (and good) habits of our mentors, our peers, or even those that we idolize online. Sometimes these habits can be exacerbated by groupthink and a feeling of malaise.
As discussed in Things Every Student Should Know Before Starting a Software Career, people are plagued by the fundamental attribution error where we constantly excuse our own possibly bad or accidental behaviors but don’t give the same courtesy to others who suffer from the same issues. Our bad behaviors are always due to our situation while others bad behaviors are due to their bad nature. Of course, the situations are rarely that simple.
But we are fortunate as humans to have the ability to learn, adapt, reason, and create. We don’t have to be stuck in our ways unless we chose to be. An important part of being a good person is continuous self reflection, honest introspection, and retrospectives of our behaviors in all walks of life. How do you expect to improve your behaviors if you never identify and honestly reflect on those behaviors?
So let’s go through this guide and see if we can knock a few bad habits loose. For those of you that don’t currently exhibit any of the below: good! But be always cautious if you find yourself slipping.
Getting Overly Defensive of Code
Let’s get something straight. You are not your code. The way in which you tackle a problem doesn’t define you as a person. In software development, there are often many ways to solve something. Yes, some are more efficient or straight forward than others, but that doesn’t negate other solutions. It is very rare for there to be a “correct” way to accomplish something.
When someone comments on the way you’ve solved something, it doesn’t have to be taken as an attack. Yes, we have seen bad actors in the communities that feel it necessary to personally insult someone over the way they’ve implemented a feature, but we find those to be in the minority and definitely not acceptable. If you find yourself in that type of situation, be the bigger person, accept or deny the criticism in some professional way, and move on to something more important.
During the code review process, some people may leave comments that are emotionless, to the point, and largely devoid of any personal mentions. Just because the reviewer is direct to their point, it doesn’t mean they dislike what you’ve done. What good is a code review process if the people involved are out to one-up each other or prove some pointless position?
The whole point of the code review process is to end up with better developers and a better. We should strive to provide honest feedback, accept honest criticism, and work together towards mutually agreeable solutions without having to making it into a sporting event.
Here are some example conversations that illustrate the sometimes overly defense nature of programmers. Before you think these are fake, we have actually seen similar conversations in the wild.
Commenting on an efficiency change:
Reviewer: Use a List here instead of a Hash Table. It’s less work than all of what you wrote.
Author: Why does that matter? I took the time to write that, so I’m not going to change it now when it works fine.
Or reminding someone of a team norm:
Reviewer: Why are you using that framework A instead of framework B? B is the standard.
Author: Your comment makes me feel like I don’t know what I’m doing. Why bring this up?
Or letting someone know of a better practice:
Reviewer: You should break this pull request up into smaller pieces. It’s impossible to review accurately as is.
Author: I spent all night working on this pull request. I’m not going to change it now. I’m the tech lead and have decided to do it this way.
In all cases, the reviewer never personally attacked the author, but it seems like the author took the comments personally. They were direct, to the point, asked poignant questions, or highlighted the best practice.
Instead of seeing the comments for what they were (nothing more than questions and statements), the author decided to bring emotional baggage to the conversation and make a much bigger scene than necessary. This leads to further mistrust or hostility between the reviewer(s) and the author. It just isn’t necessary and definitely isn’t helpful towards good discourse and ultimately a good product.
Again, we have seen these type of behavior in several companies. They aren’t as far-fetched as you might imagine.
In summary, leave that shoulder chip in your car when you park at the office each morning.
Not Documenting Your Work
There’s an ancient and equally awful meme between developers that goes something like, “Why should I have to tell you what my code does? Just read it and it will make sense.” It’s ancient because every developer of every age seems to repeat that type of nonsense (even if they are “just joking”), and it’s awful because it’s a total lie. Self documenting code doesn’t exist even if we throw a ton of memes into the indexes of Google Images.
And yet, we can sympathize with the underlying feelings even if we don’t agree with the sentiment. As developers, we’re in this industry to solve problems, write complex algorithms, squash dastardly bugs, and prove that we have what it takes to wrangle electrons into our bidding. Nobody ever told us that we’d be writing nearly equal of what amounts to extremely boring stories alongside our life’s work.
So to make ourselves feel better, we create funny memes, tell ourselves lies about how code is readable as is, and convince each other that we’ll remember everything about how an application functions when we need to return to it in six months. And each time we do this, our six-month-from-now self wishes time travel existed so they could go back in time and kick us for being so shortsighted.
We can hear you now, “I always name my variables perfectly, and my function names are self-explanatory!” Having descriptive names for variables, functions, classes, and files can only take one so far in understanding how something works. We may be able to quickly realize that a function named
calculateIncome does in fact calculate a person’s income, but that doesn’t tell us the why and how a function fits into the greater application space.
Perhaps the function started out only calculating incomes but quickly became entangled with side effects despite the better efforts of the development team. And now there’s code in that function that does all sorts of non-income related things. Six months later, you revisit the code completely forgetting about why the side effects were added, and now you’re stuck relearning where this monstrosity fits in the stack.
Or you may find yourself in a situation where you forgot all the entry points to this function. What are the initiators? Who are the consumers? Why are the consumers using this function? How do I get from a user-facing scenario to the point in which this function is executed? We can fully understand a function in isolation, but only good documentation will reveal how that function ties in with the rest of its siblings in the application ecosystem.
Between the time you originally implemented a solution and the time you need to revisit that section of code, the full breadth and context of what you were thinking at that time is completely lost. You’ve only transcribed to code enough of your mindset to make the computer perform that which you command. Everything else is useless to the computer, so why bother writing it down?
Well, now you know.
By the way, we aren’t suggesting that every line should be commented nor should novels be written about every function. Extremes are hardly useful, so we instead hope to find a good middle ground depending on the function and context in which that function is placed.
Isolating Yourself From the Team
Developers can be lonely creatures. Like some hobbits of The Shire were so proud to declare, “Keep your nose out of trouble, and no trouble’ll come to you.” That sentiment makes sense and can be quite attractive to a profession that requires stability and heavy thinking. After all, designers and developers thrive on uninterrupted periods of time in which deep, critical thought can occur.
With the proliferation of open office layouts hopefully hitting its peak soon, developers are strapped for quiet periods and spaces. We glue expensive headphones to our heads in an attempt to drown out the noise. Quiet rooms and booths have waiting lists equal to the length of time people are in the office for the day. Remote work is a hot commodity demanded by many because of its convenience but mostly because of that which we covet: potential quiet.
It’s easy to become jaded at the workplace when we force ourselves to employ these isolating techniques for months on end. Sooner or later retreat becomes less about convenience and more about necessity to daily survival. We become less willing to proactively engage and collaborate with our teams, instead focusing on the here-and-now problem at hand while missing the potentially bigger picture. And eventually our inner hermit emerges to the detriment of the rest of the team.
Good software development demands that the rank-and-file development staff engage in a daily give and take. One can imagine it as a sort of negotiation strategy in which product owners are incessant upon pie-in-the-sky features while developers rebuff the requests by reminding the team of technical limitations or guiding them in a more successful technical direction.
That said, developers shouldn’t employ solely defensive measures. A good product partner on the development team is one that thinks proactively, understands the importance and reasoning behind business requests, understands how a feature or product fits into the larger business picture, and can productively participate in the inherent back-and-forth nature of daily conversations between product and tech.
In short, developers simply can’t exist in a self-imposed vacuum. Yes, deep thought is required, and you should absolutely visit your hideaway when you need to put the pedal to the metal and meet a delivery. But isolation doesn’t have to come at the expense of collaboration. Your thoughts and topic positions are relevant, important, and critical to the success of the product.
Don’t take this advice as a demand to get on a soapbox and dictate the direction of your team. You should instead start small until you and your team are more comfortable with the openly collaborative approach. Try to feel out how to best communicate with your direct development coworkers and the extended product and business teams. It’s important to keep in mind that everyone communicates differently.
Not comfortable approaching product and business teams? Maybe you’re too new to the company and feel like your positions will be discarded as inexperienced?
First, you might be suffering from imposter syndrome. It’s common. Seek mentoring from senior technical leaders in your organization. Realize that you have self-worth and wouldn’t have been chosen to fill your current position unless you were valuable.
Second, you can avoid most anxiety by narrowing your interactions to either your team’s tech lead or your direct manager as a starting point. Use a mentor like your tech lead as a sounding board for your thoughts, ideas, and most importantly, your questions. This will help you get a feel for how the teams operate and interact, what the team norms are. Over time, you’ll have a good grasp on what to say to which person and when.
By starting small in this way, you can build up your confidence, feel the waters for how others communicate, and encourage the rest of the teams with your overwhelming awesomeness.
Not Double Checking Your Commits
Have you ever been graced with the responsibility of code reviewing a change set which includes unnecessary files, unhelpful commented lines left in the by the developer, or blatant mistakes like removing code or files for the purposes of local debugging but never meant for prime time? Well, we have. It’s a huge time waster for people that have to suffer through that type of code review.
What could have been an easy 5-10 minutes job by the originator of the changes can easily turn into hours of delays due to failed builds, failed unit tests, undoing commits, or in extreme cases, reverting pull requests and abandoning releases.
At the very least, every developer should use source control (Git) commands like
git status /
git diff or using IDEs like Visual Studio when paired with Git to keep an eye on and visualize the exact sets of files and changes that are pending to be committed.
Git even tries to hold your hand in this regard. You are required to first stage your changes and then explicitly commit those changes to the repository. And if you’re working with a remote repository like GitHub or Bitbucket, there’s even a third step of having to
git push your local commits to the remote!
Despite all the technical reassurances and checkpoints we’ve built into development and review processes, at the end of the day, the old adage of leading horses to water still holds true. It is ultimately up to each individual to be accountable to local commit reviews and verification. Obviously people can make mistakes, but we too often run into developers that habitually ignore the process of local verification let alone a cursory check of their pending or staged changes.
Here are some general tips to avoiding unnecessary headaches during code review:
- Build your project locally before committing anything. Don’t cause build breakers in the continuous integration (CI) systems.
- Write and run unit tests locally before committing anything. Don’t fail unit tests in the CI systems.
- Review pending commits locally before staging and committing. Check for files that slipped by
.gitignore, lingering self comments, or temporary code only meant to be used during local debugging sessions.
- Run through some scenarios to make sure your feature or fix actually works at run time. Even just making sure the happy path works is better than nothing.
We know these sound basic, but missing these steps happens far more often than you’d think and contribute to a lot of delays in the review and merge processes downstream.
It’s totally understandable, too. Like writing documentation, the mundane process of creating and maintaining unit test and integration tests along with having to sift one more time through changes that we’ve already spent hours on can be absolutely dreadful. As developers, we had fun building the design and solving the problem. Once completed, we want to move on to the next problem. No one wants to spoil the sensation of success by slowing down to do what amounts to proofreading.
And yet, it’s necessary, efficient (in the long run), and productive. If you can hold yourself accountable to the suggestions above, you and your team will be far happier on a daily basis.
Maritime tradition and President Harry S. Truman’s famous Oval Office desk plaque loudly proclaim that leaders bear the ultimate responsibility for decisions made under their watch.
The captain goes down with the ship.
The buck stops here!
These phrases don’t mean to imply that leaders are literally responsible for every action that occurs in the chain of command. Surely the captain can’t be held responsible for the actions of a lowly cleaning crew that causes a problem in the engine room? And Truman can’t be accountable for the decisions of someone way down on the list at the FBI.
Or can they?
At the end of the day, someone has to be responsible for problematic occurrences. The fallout from a bad decision or a mistake can’t ping-pong around the offices until the end of eternity. Otherwise, retrospectives would be useless, teams would never improve, and problems ultimately would be extremely delayed in being resolved or simply not resolved at all.
We have then come to accept that leaders should bear the moral responsibility of being held accountable to these situations. But why should a leader be willing to take on this level of responsibility? What’s in it for them?
There’s possibly a trade off to be made here. Leaders are often empowered with abilities that make them more willing to accept the fact that consequences will inevitably bubble up the chain to their domain.
For starters, leaders are given the ability to select their team. In a sufficiently large organization, this gives leaders the flexibility to appoint talented people over important matters so that the leader can focus on other issues. If the appointees make bad decisions, then ultimately the leader is responsible for appointing a bad decision maker.
Second, leaders can directly affect the types of policies that apply in an organization. Leaders that want control over policies and procedures should also be willing to be held accountable for any breakdowns or failures that arise from those processes. Imagine if a leader demands for the implementation of a policy that every employee only take one bathroom break per day in the name of “productivity.” Predictably, employees would revolt and demand changes. With that type of organizational power, leadership needs to be ready to respond to the results.
And third, leaders are looked to regarding the type of culture that will be normal in an organization. As the public face of the team, the way in which a leader conducts his or her behavior is critical in controlling the trickle down effect to the rest of the team. Nasty leader? Nasty team. Positive leader? Positive team. The aftereffects of a team that has normalized around the cultural behaviors trace back to only a single person: the leader.
While it seems obvious that decision makers should be held accountable to the consequences of those decisions, it is far too common for people to simply deflect the negative side effects and consume the positive praise.
That said, ownership mentality and ultimate accountability goes far beyond leaders. Each individual should be made responsible for their own actions. Developers are the experts on every line of code they write and should be the first person to ask when questions or problems arise with a specific feature in which they had a hand.
Yes, the technical leader may be the public face of defense and accountability when business leaders come knocking down the door. But the subject matter experts should be the first in line when the technical leader comes looking for advice, solutions, and fixes.
- Be proactive in solving a problem. Ask your leader how you can help. Take an active role in the brainstorming process and suggest solutions.
- Tell people that you are in fact the one who created something and should be helping support it.
- Take the lead on documenting features and fixes for which you are the primary developer.
- Don’t take failure personally. If a feature doesn’t work or a fix doesn’t fully function, leave your emotion at the door.