Write Better Code

There is this famous comic stating that the only valid measurement of code quality is the WTF per minute ratio.

Well… in all honesty, this is actually one of the most accurate depictions of software development. Your code will suck no matter what, but you can at least follow some basic guidelines to make maintenance easier down the road. Don’t get me wrong… Your code will still be terrible, but at least you can be proud that you gave it your best shot.

There are various schools of thought and books written on the topic of good software, and in this video we’ll look at 7 lessons from one of the most popular books on the subject. Clean Code - A Handbook of Agile Software Craftsmanship by Uncle Bob.

Let me preface this by saying that using the word “Agile” in anything software related is a big red flag for me, but we’ll let this slide for now.

Comments Do Not Make up for Bad Code

We all jumped into a codebase at some point and were surprised to find some random method which had more lines of comments than actual code.

While the author might have had good intentions, this is actually a terrible code smell, and you should mentally prepare for a frustrating reviewing and debugging session whenever a lot of comments are present. As a rule of thumb, if you feel the need to explain your code with comments, you probably need to spend more time reducing your code complexity. Good code needs no explanation. If variables and methods are correctly named, and the logic is properly implemented, any block of code should be pretty self explanatory.

Keep in mind that comments have to be maintained as well, increasing your workload in the long run. And, they can even make code more verbose when they are redundant. Java devs deserve a special medal here, since they made famous the bad habit of commenting even the getters and setters.

public class Customer {
    // Fields
    private String name;

    /**
     * Gets the name of the customer.
     * @return The name of the customer.
     */
    public String getName() {
        return name;
    }

    /**
     * Sets the name of the customer.
     * @param name The   new name of the customer.
     */
    public void setName(String name) {
        this.name = name;   

    }
}

Good Naming

I mentioned good naming of methods and variables, which is yet another important rule in programming. There are a few key concepts you should keep in mind.

  1. Variables, functions, and classes should clearly indicate their purpose. Poor names that don’t reveal intent lead to code that’s hard to understand, requiring unnecessary comments or extra context. Note however, that naming conventions might be different depending on your programming language.
let date = getDate(); // NO - expiration date
let expirationDate = getDate() // YES

In practice, naming a member instance simply “m” might be frowned upon in Java, but it is more than encouraged in Go.

Member m = new Member()
m:= Member{}
  1. You should avoid using misleading or unclear names, such as those that might be mistaken for something else or are too similar to other names. Names should be distinct and accurately reflect the purpose of the item they represent.
List<Account> accountList = new LinkedList<>();
List<Account> accounts = new LinkedList<>();
  1. Names should not only be unique but also meaningful. Avoid arbitrary distinctions like misspellings or adding noise words like “info” or “data” that don’t clarify the function or object’s purpose.
// What kind of data?
func processData(data string) {
  // ...
}

func processUserData(data string) {
  // ...
}
  1. Names should be easy to pronounce and easy to search for within the codebase. Avoid single-letter variables or cryptic abbreviations that make the code difficult to discuss or search.
// generation year, month, day, hour, minute, second
int genymdhms;

// Clear and pronounceable name
int generationTimestamp;
  1. Always add meaningful context. When names by themselves are not sufficient, provide context by placing them within well-named classes, functions, or namespaces. This helps to clarify their meaning within the broader scope of the code.
func save(name string) {
    // ...
}

func saveCustomerName(n string) {
    // ...
}
  1. As a bonus, while all these rules are important, you can actually overuse them. So find a balance and don’t over do it.
class EfficientStringHandlingController {}

Keep Things Small

Another important rule which will help you maintain your code over time is to keep things small.

To start with a small anecdote, one of the projects I worked on in the past was an online ordering solution which had a “place order” method which exceeded 2000 lines of code. Yes, this code is still in production and handles thousands of new orders daily. However, you can imagine that implementing anything new in the “place order” function is a nightmare.

Keeping functions small and focused on only one thing is a no brainer, but, in practice, you’ll realize that this is easier said than done. You’ll read about arbitrary rules like the fact that functions should not exceed 20 lines of code, which can rarely be followed in real world scenarios. However, the core principle of keeping things small should dictate your implementation, and is the main mechanism that will allow you to avoid complexity in your code.

Note that it is not enough to write good code. You also have to maintain its quality over time. Code has this bad habit of deteriorating as projects progress. To combat this, we need to actively prevent code from degrading.

Leave the campground cleaner than you found it

The Boy Scouts of America have a guiding principle that applies to software development as well: “Leave the campground cleaner than you found it.”

If every time we checked-in our code, we made it just a bit cleaner than when we checked it out, we could prevent code decay. The improvements don’t have to be major. You can rename a variable to make its purpose clearer, split a function that’s getting too big, remove a small piece of redundant code, or simplify a complex conditional statement.

As always, there is a fine line between improving code and breaking functionality, so such changes should be backed by thorough testing.

Ok, we spent some time looking at the form of the code, so let’s now take a step further into some more in depth topics.

Error Handling

One really important topic in software development is error handling. Although this is necessary in order to deal with unexpected scenarios and potential failures, it often leads to a cluttered codebase due to various checks and logic that might obscure the main functionality.

if _, err := os.Stat(filename); os.IsNotExist(err) {
  return nil, fmt.Errorf("file %s does not exist", filename)
}

data, err := ioutil.ReadFile(filename)
if err != nil {
  return nil, fmt.Errorf("could not read file %s: %v", filename, err)
}

There are numerous ways programming languages are dealing with this in practice, from Go’s error values and the somewhat verbose error handling to Rust’s special types and pattern matching.

match read_file_content("example.txt") {
    Ok(content) => println!("File content:\n{}", content),
    Err(e) => println!("Error reading file: {}", e),
}

While the approaches and suggested best practices might vary depending on the ecosystem you are in, there are a handful of simple rules you should follow to write clean code that handles errors gracefully.

  1. You should use exceptions instead of return codes. This allows you to separate the concerns of error handling and the business logic.

  2. When possible, you should always write try-catch-finally statements first. This will force you to think about potential problems and corner case scenarios upfront, ensuring the code remains in a consistent state regardless of errors.

  3. Null values are a necessary evil in software development, but you should avoid returning or passing null in your functions. Returning or passing null leads to code that is prone to NullPointerExceptions and cluttered with null checks. Instead, use special case objects or throw exceptions to avoid null-related errors.

  4. Keeping it simple is a mantra we’ll get back to in a second, but this applies for errors as well. You should define exceptions based on caller needs. So always simplify error handling by defining exceptions that align with how they will be caught, reducing redundancy and making the code more maintainable.

  5. And, of course, it goes without saying that you should provide context with your exceptions. In other words, exceptions should include meaningful error messages that clearly describe the failure, helping with debugging and logging.

KISS

Now let’s address the keep it simple principle.

I recently read about Elon Musk’s five-step engineering process, which follows this principle closely. In short, when building something you should follow five clear steps:

  1. Make the requirements less dumb;
  2. Delete the part or process step;
  3. Simplify or optimize;
  4. Accelerate cycle time;
  5. Automate.

While simplifying and optimizing is an obvious action, I find the second step more interesting. In practice, you’ll discover that sometimes the best way to simplify a process is to actually completely remove it from the workflow. And yes, this applies to coding as well, and Clean Code offers some guidance on this topic.

First, you should always avoid over-engineering, and, believe it or not, sometimes abstraction can be a pain in the butt. Second, make sure that you are actually going to need the functionality you are implementing. Of course you should always avoid duplication and, finally, follow the single responsibility principle to make sure that your classes and functions have only one reason to change. This prevents unnecessary complexity by keeping classes and functions focused on a single task.

Testing

Finally, let’s address the most hated aspect in software development - testing. As much as we despise it, testing is a crucial aspect of writing clean, maintainable, reliable code. Again, there are a few best practices you should keep in mind when forced to write tests.

You should write simple, clear tests that are easy to read and understand. You should avoid over-testing and find a balance for your test coverage. Finally, it should be obvious that edge cases should be thoroughly thought out. It’s easy for developers to only focus on happy path testing, so it is a good idea to team up for proper testing.

If you feel like you learned something, you should watch some of my youtube videos or subscribe to the newsletter.

Until next time, thank you for reading!