In the previous article of this series, we discussed the most important part when writing clean code; naming the system components such as variables, functions, classes, and methods. The importance of choosing good names has been presented with a couple of examples discussing the difference between informative and disinformative names and how the disinformative could be much of a problem than poorly chosen names. If one didn't get anything else from that article, at least this should be kept in mind.

This article would cover how to write a clean code when it comes to functions. It is good to mention that most of the parts which are applied on functions apply on methods too. However, this article will not include any advice for writing clean methods, but we will touch on that in a different article.

The examples in this article is written in python's Flask but could be applied in any other language/framework.

Small, Do One Thing, and DRY

Small

Functions should do one thing. They should do it well. They should do it only.

When approaching functions, we are aware that the approach is serving a clear reason, and that reason is to reuse a piece of code/functionality. As fundamental as this sounds, it tends to be forgotten, writing too much logic in one function and reaching a level that is as bad as a block of spaghetti code. Functions should be small, doing one thing, and contains no repetitive work. However, ensuring that the function is doing one thing is not a simple task. In order to achieve that, each statement in the function should be at the same level of abstraction as the rest of the statements.

The following scenario could serve as a good example.

def get_books_details():
    try:
        response = requests.request(
            'GET',
            'http://api.books.com/books?per_page=50'
        )
    except requests.exceptions.HTTPError:
        raise ValueError("API_FAIL")

    data = response.json()

    for _book in data['collection']['books']:
        _book['author'] = '{} {}'.format(
            _book['author_fname'], 
            _book['author_lname']
        )

    return {
        'status': response.status_code,
        'data': data['collection'],
        '_links': data['links'],
        '_meta': data['meta']
    }

def get_single_book_details(book_id):
    try:
        response = requests.request(
            'GET',
            'http://api.books.com/books/{}'.format(book_id)
        )
    except requests.exceptions.HTTPError:
        raise ValueError("API_FAIL")
    
    data = response.json()

    data['author'] = '{} {}'.format(
        data['author_fname'], 
        data['author_lname']
    )

    return {
        'status': response.status_code,
        'data': data['collection'],
        '_links': data['links'],
        '_meta': data['meta']
    }

def create_book(book_data):    
    try:
        response = requests.request(
            'POST',
            'http://api.books.com/books',
            book_data
        )
    except requests.exceptions.HTTPError:
        raise ValueError("API_FAIL")

    data = response.json()

    return {
        'status': response.status_code,
        'data': data['collection'],
        '_links': data['links'],
        '_meta': data['meta']
    }

def delete_book(book_id):    
    try:
        response = requests.request(
            'DELETE',
            'http://api.books.com/books/{}'.format(book_id)
        )
    except requests.exceptions.HTTPError:
        raise ValueError("API_FAIL")

    data = response.json()

    return {
        'status': response.status_code,
        'data': data['collection'],
        '_links': data['links'],
        '_meta': data['meta']
    }

These functions are, alongside manipulating data, performing API requests and placing the returned response data with the response code in a dictionary and returning that. Each function is doing so much, and there is a lot of repetitive work. To enhance the reusability, we can move the logic of sending API requests functionality to its own function, move structuring the returned dictionary to its function as well, and create a function that merges the first and last name of the author.

def get_books_details():
    response = _api_request(
        method='GET', 
        url='books'
    )

    data = response.json()

    processed_data = _add_author_full_name_to_books(
        books=data['collection']['books']
    )

    return _format_data_dict(
        status_code=response.status_code,
        data=processed_data
    )

def get_single_book_details(book_id):
    response = _api_request(
        method='GET', 
        url='books/{}'.format(book_id)
    )
    
    data = response.json()

    data['author'] = _get_author_full_name(
        first_name=data['author_fname'],
        last_name=data['author_lname']
    )

    return _format_data_dict(
        status_code=response.status_code,
        data=data
    )

def create_book(book_data):    
    response = _api_request(
        method='POST', 
        url='books',
        data=book_data
    )

    data = response.json()

    return _format_data_dict(
        status_code=response.status_code,
        data=data
    )

def delete_book(book_id):
    response = _api_request(
        method='DELETE', 
        url='books/{}'.format(book_id)
    )

    data = response.json()

    return _format_data_dict(
        status_code=response.status_code,
        data=data
    )

def _api_request(method, url, data={}):
    BASE_URL = 'http://api.books.com/'
    
    try:
        response = requests.request(
            method=method,
            url="{}{}".format(BASE_URL, url),
            json=params
        )
    except requests.exceptions.HTTPError:
        raise ValueError("API_FAIL")

def _format_data_dict(status_code, data):
    return {
        'status': status_code,
        'data': data['collection'],
        '_links': data['links'],
        '_meta': data['meta']
    }

def _add_author_full_name_to_books(books):
    for _book in books:
        _book['author'] = _get_author_full_name(
            first_name=_book['author_fname'],
            last_name=_book['author_lname'],
        )
    
def _get_author_full_name(first_name, last_name):
    return '{} {}'.format(first_name, last_name)

The functions are smaller (see the reduced number of lines), doing one thing only (each function has a specific task e.g. sending an API request, formatting data...), more readable, and the repetitive work has been eliminated.

Reading Top-to-Bottom

As Robert C. Martin said, the code should be naturally telling a story, and as you read a story from the beginning to the end, your code should also do so. How does this apply to the code above? Preferably, the implementation of a function that is being called inside another function should be under the caller. This will make the flow of reading a function easier, emphasizing on the abstraction level that we talked about above.

For instance, the function get_books_details send an API request by calling _api_request, then adding the author full name for each book _add_author_full_name_to_books which by itself calling _get_author_full_name to concatenate the first and last name of the author forming the full name, then lastly returning the formatted data using _format_data_dict function. Each statement in get_books_details is at the same abstraction level, increasing the code readability.

Function Arguments

Many argue about the maximum number of the arguments that the function should have, yet there is no right or wrong number. However, most agree that the less it is, the better the code. Uncle Bob said that "three arguments should be avoided where possible," so apparently he suggests that the maximum is two. Sometimes that's near to impossible if not is. But is it though?

Apparently, we can reduce the number of the arguments if we take a step back and define the reason behind each one of them.

  • Is it a flag argument?
    A flag argument is a boolean argument that makes the function take one of two ways. The Short answer to those type of arguments is to avoid them at all costs, first and for most, the function would violate the "do one thing" principle. The easiest way to avoid them is splitting the functionality into two functions.

  • Does the list of arguments belong in an object?
    When we end up in multiple arguments that have a correlation to each other, it's likely that they could be wrapped in their own class or object. Notice the _format_data_dict function. It has data as a second argument containing collection, links, and meta. If each one of these passed as an argument, we would end up with four arguments being passed to the function. In this way, it naturally came as a solution.

  • Does the argument being checked?
    If the function is answering a question about an argument, it's best to pass that argument alone to the function. For instance, a function is_file_open(file) check if a file (the argument) is open and in use. Most of these functions go under the "is" category.

Function Side Effects

This is one of the trickiest, and most hated bit when it comes to writing functions. Uncle Bob described it as telling a lie. The function promises to do one thing and yet secretly does other things such as altering the value of a passed parameter. This is destructive and damaging, and above all, makes the code untrustworthy. For example, a function check_login_credentials(username, password) that check the login credentials of a user and return true if the credentials are right or false if not, but besides that, it also initializes a session. Initializing the session is the side effect in this case, yet the name promised to check the credentials only. If this function were called in another place, it would reinitialize the session resulting in erasing the old session. In this case, the function should be renamed to check_login_credentials_and_init_session(username, password).

Conclusion

Functions are the actions of any system and should be treated as first-class citizens. It is tough to write a clean code right from the beginning; it is something happens gradually and takes time and practice. The best way of writing clean functions is to start writing them first, make them work, then go back and clean them if possible. Keep on this cycle of writing code then cleaning it, and it will become second nature. Refining the code and splitting functions to smaller ones is the key.