Skip to main content

Command Palette

Search for a command to run...

Clean Code - Part-4: Functions/Methods

Updated
8 min read
Clean Code - Part-4: Functions/Methods

In the realm of clean code, it's crucial that both the call to a function and its definition are clear and easy to understand. When invoking a function, readability is key, including clear argument orders. Equally important is the ease of working within the function itself, whether it's writing new code or maintaining existing functions. Therefore, the number and order of arguments play a significant role in how readable and manageable a function is to use. Additionally, the length of the function body matters, as it affects the complexity of understanding and maintaining the code within.


Calling a function

Let's delve into a more comprehensive explanation of the principles and practices surrounding function parameters in the context of writing clean and maintainable code.

Consideration of parameters

Parameters are the inputs that functions accept to perform their tasks. When designing functions, the number and order of parameters play a pivotal role in their usability and readability.

Minimizing the number of parameters is a cardinal rule in clean code practices. Reducing parameter count simplifies the function interface, making it easier to understand and use. Developers are less likely to make mistakes when calling functions with fewer parameters, as there's less cognitive overhead in remembering their order and purpose.

Refactoring with objects

One effective technique for reducing parameter count is to bundle related parameters into an object. By encapsulating parameters within an object, you create a cohesive data structure that represents the inputs to the function. This approach improves readability by providing a clear and self-descriptive interface for invoking the function.

Let's consider a practical example to illustrate this concept. Suppose you have a logging function that logs messages, along with an optional flag indicating whether the message is an error. Initially, you might define the function with separate parameters for the message and error flag:

def log(message, is_error=False):
    if is_error:
        print(f"Error: {message}")
    else:
        print(message)

However, as the function evolves and additional parameters are introduced, the function call becomes less intuitive and more error-prone.

To address this, you can refactor the function to accept a single parameter—an object representing the logging options:

class LogOptions:
    def __init__(self, message, is_error=False):
        self.message = message
        self.is_error = is_error

def log(options):
    if options.is_error:
        print(f"Error: {options.message}")
    else:
        print(options.message)

With this refactoring, calling the log function becomes more expressive and readable:

log(LogOptions("hi", is_error=False))

While this technique aligns particularly well with functional programming principles, where functions often operate on single data structures, it's also applicable in object-oriented programming. By adopting this approach, you create cleaner and more maintainable code that adheres to the principles of encapsulation and separation of concerns.

Exceptions

It's important to note that there are exceptions to the guideline of minimizing parameter count. Functions that accept a dynamic number of arguments, such as Python's *args and **kwargs, may have a larger parameter count by necessity.

Naming

Lastly, when naming functions and their parameters, prioritize clarity and descriptiveness. Meaningful names enhance the readability of the codebase, making it easier for developers to understand and maintain.

In conclusion, by carefully considering the number and order of function parameters and leveraging techniques such as parameter bundling with objects, developers can write cleaner, more readable, and maintainable code. These practices ultimately contribute to the long-term sustainability and scalability of software projects.


Writing a function body

In software development, the length of a function's body has a profound effect on code quality and maintainability. Long, complex functions tend to obscure the logic, making it difficult for developers to grasp the overall flow and purpose. On the other hand, breaking down functions into smaller, focused units of work enhances readability, promotes code reuse, and facilitates easier debugging and testing.

Single Long Body vs. Distributed Responsibilities

Bad Example: A single long body function encapsulates multiple tasks within its structure, leading to a tangled web of logic that's challenging to comprehend at a glance. This monolithic structure intertwines various tasks, making it challenging to understand and modify.

Good Example: Distributing responsibilities across multiple smaller functions results in a more modular and readable codebase. Each function focuses on a single task, making it easier to understand and modify.

Pros of Distributing Responsibilities

  1. Modularity: Breaking down functions into smaller units promotes modularity, allowing developers to understand and modify individual components without impacting the entire codebase.

  2. Readability: Smaller functions are easier to read and comprehend, as they focus on specific tasks and maintain a clear, linear flow of execution.

  3. Reusability: Modular functions can be reused across different parts of the codebase, reducing duplication and promoting a more efficient development process.

Guide for how to distribute responsibilities

Deciding when & how to split a function effectively is a critical aspect of writing clean and maintainable code. When breaking down functions, it's essential to adhere to the principle of "each function should only do one thing." Here's a detailed guide on how to decide when and how to split functions, while also considering levels of abstraction:

  1. Single Responsibility Principle (SRP):

    Each function should have a single responsibility or perform a single task. When assessing whether to split a function, consider whether it's responsible for more than one distinct operation or task. If so, it's a clear indication that the function should be broken down into smaller, more focused units.

  2. Levels of Abstraction:

    Functions should operate at a consistent level of abstraction, meaning that the operations they perform should be at a similar conceptual level. For example, a function named saveUser(user) should handle operations related to saving a user, such as validating input data or persisting user information to a database.

     def saveUser(user):
         if isEmailValid(user.email):  # Level of abstraction: Validation
             # Save user to database       # Level of abstraction: Persistence
             pass
         else:
             # Handle invalid email       # Level of abstraction: Error handling
             pass
    
     def isEmailValid(email):
         # Validate email address
         pass
    

    In this example, saveUser(user) delegates the task of email validation to isEmailValid(email), ensuring that each function operates at a consistent level of abstraction.

  3. Functions Should Do Work One Level Below Their Name:

    A function's name should provide a clear indication of the task it performs, and the function itself should handle operations one level below that abstraction. For instance, a function named saveUser(user) should encapsulate operations related to saving a user, such as validating user input data, checking email validity, and persisting user information to a database.

  4. Avoid mixing levels of abstraction:

    Avoid mixing operations with different levels of abstraction within the same function. For example, a function responsible for user authentication should not handle low-level tasks such as database access or input validation. By keeping functions focused on a single level of abstraction, code becomes more modular, readable, and maintainable.

  5. Write pure functions:

    A pure function is one that has no side effects and always produces the same output for a given input. This property makes pure functions predictable, testable, and easier to reason about. By isolating their behavior from the rest of the system, pure functions promote code reliability and maintainability.

  6. Avoid side effects:

    Side effects occur when a function modifies state outside of its scope, such as changing global variables, modifying external resources, or printing to the console. While some side effects are necessary, such as updating a database or interacting with external services, excessive side effects can make code difficult to understand and maintain.

    When a function has side effects, it's crucial to reflect this in its name. Descriptive function names can provide clarity about the potential side effects, helping developers understand the function's behavior and usage. For example, a function named updateDatabase implies that it may modify the database state.

    Alternatively, if a function performs side effects, consider encapsulating the side effect in a separate function or module. This approach isolates side effects, making them easier to manage and reason about. The pure function can then call the side effect function without directly modifying state.

Guide for when to distribute responsibilities

Deciding when to split functions depends on several factors:

  1. Complexity:

    If a function becomes too complex or lengthy, it's often an indication that it should be split into smaller, more manageable units.

  2. Single Responsibility:

    If a function performs multiple tasks or operations, it's a clear sign that it should be broken down into smaller functions, each with a single responsibility.

  3. Reusability:

    Identify portions of code that could be reused in other parts of the codebase. Extracting reusable logic into separate functions promotes code reuse and maintains a DRY (Don't Repeat Yourself) codebase.

Guide for when not to distribute responsibilities

Avoiding the unnecessary splitting up of functions is crucial to maintaining a balance between granularity and readability. While breaking down functions can enhance code organization and clarity, excessive fragmentation can lead to code that is difficult to navigate and understand. Here's a more detailed explanation of when to avoid uselessly splitting up functions:

  1. Granularity vs Readability:

    While granularity can improve code organization and reusability, it won't automatically enhance readability in all cases. Breaking down functions into excessively small units can result in code that is difficult to follow, especially if the functions are named poorly or if the split does not correspond to logical divisions in the code.

  2. Renaming the Operation:

    If splitting a function results in merely renaming the operation without providing additional clarity or modularity, it may not be necessary. For example, if a function named calculateTotalPrice is split into calculateSubtotal and calculateTax, but the original function's purpose remains unchanged, the split may not add value.

  3. Finding the New Function Takes Longer:

    If locating the newly extracted function takes longer than reading the extracted code inline, it defeats the purpose of splitting the function. The goal of function extraction is to enhance readability and maintainability, so if the extracted function introduces unnecessary complexity or confusion, it should be avoided.

  4. Can't Produce Reasonable Name:

    If you struggle to produce a reasonable, descriptive name for the extracted function, it may indicate that the operation being extracted does not have sufficient coherence or significance to warrant its own function. Functions should encapsulate meaningful units of work, so if the extracted function lacks clear purpose or relevance, it may be better to keep the operation within the original function.

In conclusion, clean code is not just about writing code that works; it's about writing code that is easy to understand, maintain, and extend. When it comes to functions and methods, readability is paramount. Calling a function should be intuitive, with clear argument orders and concise naming conventions. Moreover, the length of a function's body plays a significant role in readability. By distributing responsibilities effectively and adhering to the single responsibility principle, functions become easier to comprehend and manage.