Showing posts with label programming. Show all posts
Showing posts with label programming. Show all posts

Thursday, October 3, 2024

The Unwritten Laws of Code Reviews: Lessons Learned the Hard Way

When I first got into the habit of reviewing code, I thought I had it figured out. After all, code reviews are just about making sure things work, right?

How little did I know how wrong I was.

My attitude to code reviews wasn't just inefficient, it was damaging: it irritated colleagues, fomented resentment, and delayed feature launches, all for failing to stick to some really simple yet powerful unwritten rules of code reviews.



Here are the 4 rules I learned the hard way:

1. Block a PR Only If It Requires Your Approval

We've all been there: you click "Request Changes" because something seems off, or you don't like some implementation detail. It sounds harmless-after all, it's just a request, right?


Wrong.


Where you "request changes," that action often halts the entire process. And let's be real: there's a reason it's written in red.


So unless the change being requested is literally the only thing preventing you from advance with a bug or security vulnerability or something imperative, don't block the pull request. 


Suggestions are well and good, but over-zealously blocking PRs gets in the way of frustrating your peers and slowing them down.

2. Show Don't Tell

I have no idea how much time I wasted typing away explaining changes with long paragraphs when a code snippet would do.


Instead of writing "You should refactor this section to be more DRY," you can simply write the refactored code and paste it in the comments. It's quicker and easier, and helps the developer understand your point straight away. Code is the common language we all understand, so use it to your advantage.

3. Give Critical Feedback In Person (or Via a Slack Huddle)

There is nothing worse than getting back a five-paragraph essay tearing your code apart. Code review platforms are great for comments, but can often strip out much of the nuance that a conversation provides.


If you need to provide critical or serious feedback, it's best to talk about it in person or through a quick Slack huddle. That way, it's much easier to convey your tone and avoid misunderstanding. Moreover, with a direct yet respectful conversation, you are less likely to bruise an ego. 

4. Review the Big Picture First

It's tempting to dive into the details-after all, nitpicking code is what we love doing! But before you get into the small stuff, take a step back. Look at the high-level changes:

  • Are there any API changes?
  • Is the database schema being modified?
  • Do the changes impact overall architecture?

Focusing on the big-picture stuff first helps to identify the potential blockers earlier on. As for the small stuff - those minor refactors, naming conventions, or unit tests ,they often can be cleaned up later.

As a matter of fact, those little details could well change based on larger discussions about the big picture anyway.

The Most Important Rule: Everyone Has a "Nit Limit"

Everyone has a limit in terms of how much nitpicking they are willing to tolerate. We all want to improve our code, but we all reach a stage where too much feedback turns into resentment. People won't always be open about their feelings of frustration, and they might not even be aware that it's happening in their brain, but the more nitpicks you do, the less they'll care about your feedback.


So, pick your battles. Don't die on a hill for that one extra line of spacing or whether to use a const instead of a let.


When you approach code review, bring curiosity and empathy. Try to understand why the author decided on one thing over another. Most of all, this holds true if you will be the maintainer of this code later. Instead of "this is wrong," start off by "why." Understanding of trade-offs is key in software development.

Conclusion: Give Helpful, Respectful Feedback and Everyone Wins

Code reviews are about so much more than finding mistakes at the end of the day. It's about collaboration, improvement, and making sure we're all building something maintainable. If done right, code reviews can build trust, solidify team cohesion, and ultimately drive better results for us all.

And don’t forget - always squash your commits.


Friday, September 29, 2023

Defensive Programming in TypeScript: Building Robust and Secure Applications

Defensive programming is the art of writing software that not only works under ideal conditions but also gracefully handles unexpected situations, errors, and vulnerabilities. In TypeScript, a statically-typed superset of JavaScript, defensive programming is a critical practice for building applications that are both robust and secure. In this blog post, we'll explore the key principles and practices of defensive programming in TypeScript, using code examples to illustrate each concept.

1. Input Validation

Input validation is the first line of defense against unexpected data. Always validate and sanitize user inputs to prevent potentially harmful or incorrect data from causing issues. Let's see how you can do this in TypeScript:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function validateEmail(email: string): boolean {
  const emailRegex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
  return emailRegex.test(email);
}

const userInput = "user@example.com";
if (validateEmail(userInput)) {
  console.log("Email is valid.");
} else {
  console.error("Invalid email.");
}

2. Error Handling

Comprehensive error handling is crucial for gracefully managing unexpected situations. Use exceptions to handle errors effectively:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function divide(a: number, b: number): number {
  if (b === 0) {
    throw new Error("Division by zero is not allowed.");
  }
  return a / b;
}

try {
  const result = divide(10, 0);
  console.log(`Result: ${result}`);
} catch (error) {
  console.error(`Error: ${error.message}`);
}

3. Boundary Checking

Ensure that data structures are accessed within their specified boundaries to prevent buffer overflows and memory issues:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function safeArrayAccess(arr: number[], index: number): number | undefined {
  if (index >= 0 && index < arr.length) {
    return arr[index];
  }
  return undefined; // Index out of bounds
}

const array = [1, 2, 3];
const index = 5;
const element = safeArrayAccess(array, index);
if (element !== undefined) {
  console.log(`Element at index ${index}: ${element}`);
} else {
  console.error("Index out of bounds");
}

This code checks whether the index is within the valid range before accessing the array.

4. Resource Management

Properly manage resources like memory, files, and network connections, including releasing resources when they are no longer needed to prevent resource leaks:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class ResourceHandler {
  private resource: any;

  constructor() {
    this.resource = acquireResource();
  }

  close() {
    releaseResource(this.resource);
  }
}

const handler = new ResourceHandler();
// Use the resource
handler.close(); // Release the resource

In this example, we acquire and release a resource using a ResourceHandler class to ensure resource cleanup.

5. Fail-Safe Defaults

Provide default values or fallback behavior for situations where input or conditions are missing or invalid:

1
2
3
4
5
6
7
8
function getUserRole(user: { role?: string }): string {
  return user.role || "guest";
}

const user1 = { role: "admin" };
const user2 = {};
console.log(getUserRole(user1)); // "admin"
console.log(getUserRole(user2)); // "guest"

The getUserRole function returns a default role if the user object lacks a role property.

6. Assertions

Use assertions to check for internal consistency and assumptions within the code. Assertions can help catch logic errors during development:

1
2
3
4
5
6
7
function divide(a: number, b: number): number {
  console.assert(b !== 0, "Division by zero is not allowed.");
  return a / b;
}

const result = divide(10, 0);
console.log(`Result: ${result}`);

7. Code Reviews

Encourage code reviews by peers to identify potential issues and vulnerabilities in the codebase. Code reviews help ensure that defensive programming practices are consistently applied across the project.

8. Testing

Develop thorough test suites, including unit tests and integration tests, to validate the correctness and robustness of the code. Automated testing can catch issues and regressions, ensuring the code behaves as expected.

9. Minimize Dependencies

Limit external dependencies to reduce the chance of compatibility issues or security vulnerabilities from third-party libraries. Use well-vetted and maintained libraries, and keep them up to date to mitigate potential risks.

10. Security Considerations

Be aware of common security issues, such as SQL injection, cross-site scripting (XSS), and cross-site request forgery (CSRF), and implement security measures to mitigate these risks. Validate and sanitize user input to prevent security vulnerabilities.

11. Documentation

Provide clear and up-to-date documentation for the codebase, including how to use the software and its various components. Documentation is essential for maintainability and understanding the defensive measures in place.

Conclusion

Defensive programming in TypeScript involves a range of practices to create reliable and secure software. By validating inputs, handling errors, and managing resources properly, you can build applications that gracefully handle unexpected situations and provide a better user experience. Always be mindful of potential issues and vulnerabilities, and apply defensive programming principles to mitigate risks and ensure the robustness of your TypeScript code.

Remember that while TypeScript helps catch certain types of errors during development, defensive programming remains essential to address a wider range of issues that might occur during runtime.