I have seen many ugly functions over my decade of software development.
One keystone of clean code is the art of function design.
These poor functions are anywhere from 100 lines long or have naming conventions that do not make sense.
Don't get me started about functions that use static global variables from other files.
I will share my best advice on creating clean and high-performing functions in this blog.
The Virtue of Small, Focused Functions
This argument is spicy, but it is a take I fully agree with.
A fundamental principle of clean code is to keep functions small and focused. The idea is simple: a function should do one thing well and do it only.
I know someone will comment and say this needs to be corrected. However, it does not. Write it well and write it cleanly.
But what does this look like in practice?
Consider a function that calculates the area of a circle. The function's sole purpose is to perform this calculation, given the radius:
def calculate_circle_area(radius):
pi = 3.14159
return pi * (radius ** 2)
In this example, calculate_circle_area adheres to our function principle. It has a single responsibility: to compute the area of a circle based on the provided radius.
Its simplicity and focus make it transparent and testable.
This function is easy to read, understand, and test.
It does one thing: it calculates the area of a circle, making it a perfect example of a focused function.
What if we wanted to add more shapes to this function?
def calculate_area(shape, x, y=None):
if shape == "circle":
pi = 3.14159
return pi * (x ** 2) # x is treated as radius here
elif shape == "rectangle":
return x * y # x and y are treated as length and width
First off the comments are only here because the function is doing too much.
This function, calculate_area, tries to do too much. It calculates areas for multiple shapes based on the inputs, complicating its logic and increasing the risk of errors.
The function's multiple responsibilities make it harder to understand, test, and maintain.
If you believe you are starting to do multiple things within your function, that should be a red flag that the function is getting too long.
Multiple actions inside a function violate the single responsibility principle by incorporating logic for different shapes in one function. So, let's dive into The Single Responsibility Principle.
Single Responsibility Principle (SRP)
The Single Responsibility Principle (SRP) is one of the 5 SOLID principles. SRP is intended to promote cohesion and separation of concerns in software modules.
This principle is about limiting the impact of modifications and simplifying understanding and testing.
from datetime import datetime
def format_date(date):
return date.strftime("%Y-%m-%d")
def get_current_date_formatted():
current_date = datetime.now()
return format_date(current_date)
Here, format_date is solely responsible for formatting a given date. get_current_date_formatted retrieves the current date and uses format_date to format it. Each function has a clear, singular responsibility.
Separating concerns makes the code more reusable, testable, and maintainable.
Changes in the date formatting logic will only require modifications to format_date, not get_current_date_formatted.
Now, a counter-example that combines multiple responsibilities:
from datetime import datetime
def get_and_format_current_date():
current_date = datetime.now()
formatted_date = current_date.strftime("%Y-%m-%d")
print(f"Current Date: {formatted_date}")
return formatted_date
This function retrieves the current date, formats it, and prints it. It's doing three different things, which makes it more complex and less reusable.
If you need to change how the date is formatted or retrieved, you're modifying a function that also controls how the date is printed.
It mixes levels of abstraction and responsibilities, making the function more prone to errors and more challenging to maintain.
Avoiding Side Effects and Ensuring Predictability
This principle is pivotal for writing clean, maintainable, and reliable code.
A function or method is pure if it consistently returns the same output for the same set of inputs and does not cause any observable side effects.
Consider a function that calculates the total price of an order, given the unit price and quantity. A pure function for this would look like:
def calculate_total_price(unit_price, quantity):
return unit_price * quantity
This function is pure because it always produces the same output for the same input and does not modify any external state or cause side effects.
The predictability of calculate_total_price makes it easy to test and verify. You can call it with any set of valid inputs and expect a consistent output every time, simplifying debugging and refactoring.
Now, let's consider a function that not only calculates the total price but also updates a global dictionary tracking all order totals:
order_totals = {}
def update_and_get_total_price(order_id, unit_price, quantity):
total = unit_price * quantity
order_totals[order_id] = total # Side effect: modifying a global state
return total
This function causes a side effect by modifying the global order_totals dictionary. Its behavior and the value it returns could vary based on the context in which it's called and the current state of order_totals.
The update_and_get_total_price function is unpredictable and more problematic to test because its correctness depends on its inputs and the external state.
The side effect also makes the function's behavior less transparent, increasing the cognitive load on the developer.
By focusing on creating small, focused functions, adhering to the Single Responsibility Principle, and minimizing side effects, you can significantly enhance the clarity, quality, and reliability of your code
Good luck on your coding journey friend!
Cheers,
Eric
I like your take on this, Eric. But when I started telling people on PRs to split functions and use SRP they became a bit skeptical and defensive.
Some had good points though, like for example, that writing small functions makes the code navigation hard, or hard to follow and read.
I think it's fair but also, the contradictory argument that big functions are hard to follow is fair!
It's a bit of a paradox 😅
What do you think about that? 🤔