Writing Clean Code: Mastering Functions for Better Codebase

Photo by Sigmund on Unsplash

Writing Clean Code: Mastering Functions for Better Codebase

In the world of software development, functions play a pivotal role in shaping the quality and maintainability of your codebase. Just like the chapters of a book that make up a compelling story, functions form the building blocks of a well-structured and readable program.

1. Functions Should Be Small

Imagine reading a novel with enormous paragraphs and no clear breaks. Chances are, you'd quickly lose track of the plot. Similarly, functions should be concise and focused, performing a single task with precision. The idea behind this principle is to make functions small enough that they can be easily comprehended at a glance. When a function adheres to this principle, it's easier to understand, test, and debug.

Code Example 1:

# Bad example - Large function
def process_data(data):
    # complex processing logic...
    # more code...
    # even more code...

# Good example - Small, focused function
def validate_input(input_data):
    # validation logic...

def preprocess_data(data):
    # preprocessing logic...

2. Do One Thing

Robert C. Martin's mantra, "FUNCTIONS SHOULD DO ONE THING. THEY SHOULD DO IT WELL. THEY SHOULD DO IT ONLY," encapsulates the essence of writing effective functions. A function should have a clear and singular purpose, encapsulating a specific piece of functionality. This not only enhances readability but also makes it easier to reuse and maintain the code.

Code Example 2:

# Bad example - One function doing multiple things
def process_data(data):
    # process data...
    # generate report...
    # update statistics...

# Good example - Separate functions for each task
def process_data(data):
    # process data...

def generate_report(data):
    # generate report...

def update_statistics(data):
    # update statistics...

3. One Level of Abstraction per Function

When reading a book, it's easier to follow the storyline when it's organized in a logical sequence. Similarly, code should be organized with a consistent level of abstraction throughout the function. The "Stepdown Rule" suggests that code within a function should be organized in a top-down manner, with higher-level abstract actions calling lower-level ones.

Code Example 3:

# Bad example - Mixing abstraction levels
def process_data(data):
    # high-level actions...
    # low-level actions...

# Good example - Clear stepdown in abstraction
def process_data(data):
    high_level_process(data)

def high_level_process(data):
    # high-level actions...

def low_level_process(data):
    # low-level actions...

4. Switch Statements

Switch statements can become a breeding ground for complexity. They often violate the principle of doing one thing. Instead of using switch statements, opt for polymorphism or strategy patterns to achieve the same result while keeping your functions clean and focused.

Code Example 4:

# Bad example - Switch statement
def process_data(data, format_type):
    if format_type == 'csv':
        # process as CSV...
    elif format_type == 'json':
        # process as JSON...
    elif format_type == 'xml':
        # process as XML...
    # more cases...

# Good example - Polymorphism
class DataProcessor:
    def process(self, data):
        pass

class CSVProcessor(DataProcessor):
    def process(self, data):
        # process as CSV...

class JSONProcessor(DataProcessor):
    def process(self, data):
        # process as JSON...

class XMLProcessor(DataProcessor):
    def process(self, data):
        # process as XML...

5. Use Descriptive Names

Function names should be meaningful and self-explanatory. A well-chosen name provides a clear indication of what the function does, reducing the need for additional comments. This practice aids both in understanding the code's purpose and in maintaining the code over time.

Code Example 5:

# Bad example - Unclear function name
def a():
    # unclear logic...

# Good example - Descriptive function name
def calculate_average(scores):
    # logic to calculate average...

6. Function Arguments

Functions can have various numbers of arguments, but it's crucial to strike a balance. Functions with numerous arguments can become hard to manage and prone to errors. Aim to keep the number of arguments to a minimum, ideally three or fewer. If a function requires more inputs, consider wrapping them in a data structure like a dictionary or a class.

Code Example 6:

# Bad example - Too many arguments
def process_data(data, config, options, settings):
    # processing logic...

# Good example - Fewer arguments
class DataProcessor:
    def process(self, data, options):
        # processing logic...

7. Have No Side Effects

A function's primary role should be to compute and return a value, without altering the state of the system or causing side effects. This improves predictability and testability of your code. Avoid functions that change global variables, modify external data, or have hidden effects.

Code Example 7:

# Bad example - Function with side effect
def update_global_counter(value):
    global_counter += value

# Good example - Function without side effect
def add(a, b):
    return a + b

8. Command Query Separation

Following the principle of Command Query Separation, functions should either perform an action (a command) or provide information (a query), but not both. Mixing these roles can lead to confusion and unexpected behavior. Separating these concerns makes the code easier to understand and maintain.

Code Example 8:

# Bad example - Function performing both action and query
def process_and_get_result(data):
    # process data...
    return result

# Good example - Separate action and query
def process_data(data):
    # process data...

def get_result():
    return result

9. Prefer Exceptions to Returning Error Codes

When handling errors, prefer using exceptions rather than returning error codes. Error codes can clutter the code and make it harder to read. Exceptions provide a more structured and controlled way to handle exceptional cases.

Code Example 9:

# Bad example - Returning error code
def divide(a, b):
    if b == 0:
        return -1  # Error code
    return a / b

# Good example - Raising exception
def divide(a, b):
    if b == 0:
        raise ZeroDivisionError("Cannot divide by zero")
    return a / b

10. Don't Repeat Yourself (DRY)

Duplication in code is a recipe for inconsistency and maintenance nightmares. When you find yourself writing the same logic in multiple places, it's time to refactor that logic into a separate function. This promotes code reusability and ensures that changes need to be made in just one place.

Code Example 10:

# Bad example - Repeated logic
def calculate_area(radius):
    return 3.14 * radius * radius

def calculate_circumference(radius):
    return 2 * 3.14 * radius

# Good example - Reusable function
def calculate_circle_properties(radius):
    area = calculate_area(radius)
    circumference = calculate_circumference(radius)
    return area, circumference

11. Structured Programming

Structured programming principles guide you to write clean functions that have a single entry point and a single exit point. This avoids tangled code paths and enhances readability.

Did you find this article valuable?

Support Ayoush Chourasia by becoming a sponsor. Any amount is appreciated!