Skip to main content

Command Palette

Search for a command to run...

Building Better Code as a Software Engineer Part 2

Guidelines for Readability, Predictability, Safety, Modularity, and Reusability in Software Development

Updated
5 min read
Building Better Code as a Software Engineer Part 2
A

As a continuous learner, I’m always exploring new technologies and best practices to enhance my skills in software development. I enjoy tackling complex coding challenges, whether it's optimizing performance, implementing new features, or debugging intricate issues.

Writing code that lasts means focusing on principles that make it clear, predictable, and adaptable. Part 2 of Good Code, Bad Code guides us through practical concepts that build on theoretical foundations, ensuring our code remains usable, modular, and intuitive for other developers. Here’s a breakdown of these core ideas: code readability, avoiding surprises, preventing misuse, modular design, and code reusability.


1. Make Code Readable: Communicating Through Code

Readable code is essential for anyone who may work with your code in the future, including yourself. Readability focuses on clarity and ease of understanding, reducing the cognitive load required to follow logic.

Example of Improving Code Readability: Consider a function that calculates the area of a circle.

Unreadable Code:

def calc(x):
    return 3.1415 * x * x

The intent here is unclear. We see that x is squared, but what does it represent? And what is 3.1415? Without context, this is confusing.

Readable Code:

import math

def calculate_circle_area(radius):
    """Calculates the area of a circle given its radius."""
    return math.pi * radius ** 2

In the revised version:

  • Variable names are descriptive, making it clear what the function expects.

  • The formula uses math.pi for better readability and precision.

  • A docstring provides context, which makes the function’s purpose and parameters clearer.

Tips for Readable Code:

  • Use meaningful names for variables, functions, and classes.

  • Add comments sparingly to explain complex logic.

  • Avoid deep nesting by breaking code into smaller, more manageable functions.


2. Avoid Surprises: Making Code Predictable

Predictability in code means that it behaves as expected without hidden behaviors or side effects. Surprising code is hard to debug, easy to misuse, and can lead to unexpected bugs.

Example of Unpredictable Code:

def add_item(item, lst=[]):
    lst.append(item)
    return lst

This code has a hidden surprise: the default list (lst= []) is mutable and will retain items across multiple function calls.

Predictable Code:

def add_item(item, lst=None):
    if lst is None:
        lst = []
    lst.append(item)
    return lst

Now the function behaves predictably—each time it’s called without an argument for lst, a new list is created. This eliminates side effects and ensures each function call is isolated.

Practical Ways to Avoid Surprises:

  • Avoid mutable default arguments.

  • Limit global variables to reduce unintended interactions.

  • Follow standard conventions so the code is easy to follow and less likely to mislead.


3. Make Code Hard to Misuse: Safeguarding Code from Misinterpretation

Designing code to be hard to misuse involves writing functions and modules that guide the user toward correct usage, reducing the potential for errors and unintended consequences.

Example of Code that’s Easy to Misuse:

def process_data(data, logging=False):
    if logging:
        print("Logging data processing")
    # process data

The logging flag is unclear. Does False mean it won’t log? What does it log, and where?

Hard-to-Misuse Code:

from typing import Optional

def process_data(data, logger: Optional[callable] = None):
    """Processes data and logs if a logger function is provided."""
    if logger:
        logger("Data processing started")
    # process data

With the revised code:

  • The logger parameter is more flexible; users can pass any logging function.

  • Optional types (e.g., Optional[callable]) clarify acceptable input.

  • Docstrings provide guidance on usage, reducing potential confusion.

Strategies to Make Code Hard to Misuse:

  • Define clear parameter types and names to prevent accidental misuse.

  • Provide documentation or comments to clarify non-obvious behaviors.

  • Use function signatures that guide correct usage (e.g., required vs. optional parameters).


4. Make Code Modular: Breaking Down Complexity

Modular code is organized into self-contained parts, each with a single responsibility. This reduces complexity and makes it easier to maintain and test.

Example of Poorly Modularized Code:

def manage_employee(employee):
    # Update employee info
    employee.update_info()
    # Calculate salary
    salary = employee.calculate_salary()
    # Generate report
    report = generate_report(employee, salary)
    # Save to database
    save_to_database(report)
    return report

This function does too many things, violating the Single Responsibility Principle. It updates, calculates, generates, and saves—all in one function.

Modularized Code:

def update_employee_info(employee):
    employee.update_info()

def calculate_salary(employee):
    return employee.calculate_salary()

def generate_employee_report(employee, salary):
    return generate_report(employee, salary)

def save_employee_report(report):
    save_to_database(report)

def manage_employee(employee):
    update_employee_info(employee)
    salary = calculate_salary(employee)
    report = generate_employee_report(employee, salary)
    save_employee_report(report)
    return report

This breakdown makes the code modular, so each function has a single responsibility. Testing is now more straightforward, and each part can be reused independently.

Benefits of Modularity:

  • Improves testability by isolating functionality.

  • Simplifies debugging since smaller functions are easier to trace.

  • Enhances reusability by creating functions that can serve multiple contexts.


5. Make Code Reusable and Generalized: Creating Versatile Solutions

Reusable code reduces redundancy by creating generalized functions that can be adapted for multiple contexts. Generalization involves designing functions that are flexible enough to handle various cases.

Example of Non-Reusable Code:

def calculate_discount(price):
    discount = 0.1  # 10%
    return price * (1 - discount)

This code calculates a discount, but only for a fixed rate. What if we need to calculate different discounts?

Generalized Code:

def calculate_discount(price, discount_rate=0.1):
    """Calculates discounted price based on a given discount rate."""
    return price * (1 - discount_rate)

Now, calculate_discount is reusable for various discount rates and can adapt to different cases.

Tips for Writing Reusable and Generalized Code:

  • Use parameters instead of hard-coded values.

  • Design for flexibility by accepting multiple types or ranges of input.

  • Avoid assumptions about specific inputs to make code more adaptable.


Final Thoughts: Practical Steps to Better Code

By focusing on readability, predictability, safety, modularity, and reusability, your code becomes a practical asset rather than a technical burden. These practices in Good Code, Bad Code Part 2 provide concrete steps for writing code that is accessible, reliable, and adaptable for future needs. Embrace these principles, and you’ll be crafting code that stands the test of time and collaboration.