Decorators design pattern
Table of Contents
In software development, we often need to add extra behavior to existing functions without changing their original code. Common examples include logging, authentication, validation, caching, and performance tracking.
A beginner solution is usually copying the same logic into multiple functions. While this works at first, it quickly creates repeated code, cluttered functions, and maintenance problems. This is where the Decorator Pattern becomes useful.
What is the Decorator Pattern? #
The Decorator Pattern is a structural design pattern that allows developers to dynamically add new behavior to existing objects or functions without modifying their original implementation.
It follows the Open/Closed Principle:
- Open for extension
- Closed for modification
Instead of editing the original function directly, we wrap it with another function that adds additional behavior.
Think of it like wrapping a gift:
- The gift stays the same
- The wrapper adds something extra around it
The Problem: Adding Execution Time Tracking #
Imagine you are building a web application with functions like:
def fetch_user_profile():
print("Fetching profile...")
def process_payment():
print("Processing payment...")
Later, a new requirement appears:
“Track how long each function takes.”
A straightforward solution is manually adding timing logic everywhere.
import time
def fetch_user_profile():
start = time.time()
print("Fetching profile...")
end = time.time()
print(f"Took {end - start} seconds")
This approach introduces several problems:
- Repeated code
- Harder maintenance
- Functions become cluttered
- Utility logic mixes with business logic
If this pattern is repeated across dozens of functions, the codebase becomes difficult to maintain.
The Solution: Decorators #
Decorators solve this problem by wrapping the original function with extra behavior. The original function remains clean while the wrapper handles the additional logic.
Python supports decorators naturally because functions are first-class objects. This means functions can be:
- Passed into other functions
- Returned from functions
- Stored in variables
Here is a simple example:
def greet():
return "hello"
def make_loud(func):
def wrapper():
result = func()
return result.upper()
return wrapper
loud_greet = make_loud(greet)
print(loud_greet())
Output:
HELLO
The original greet() function stays unchanged. The wrapper adds new behavior around it.
Building a Real Decorator #
Now let’s create a reusable timing decorator.
import time
def timer_decorator(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper
This decorator:
- Receives a function
- Wraps it inside another function
- Adds timing logic
- Returns the enhanced function
Now we can apply it using the @ syntax.
@timer_decorator
def fetch_user_profile():
print("Fetching profile...")
@timer_decorator
def process_payment():
print("Processing payment...")
fetch_user_profile()
process_payment()
Output:
Fetching profile...
fetch_user_profile took 0.0001 seconds
Processing payment...
process_payment took 0.0001 seconds
The functions remain clean while the decorator handles the timing functionality.
Understanding the @ Syntax #
This:
@timer_decorator
def login():
print("Logged in")
is simply shorthand for:
def login():
print("Logged in")
login = timer_decorator(login)
The @ syntax makes decorators easier to read and apply.
Decorators with Arguments #
Good decorators should work with any function signature. That is why decorators commonly use:
*args
**kwargs
Example:
def debug(func):
def wrapper(*args, **kwargs):
print("Arguments:", args, kwargs)
result = func(*args, **kwargs)
print("Result:", result)
return result
return wrapper
@debug
def add(a, b):
return a + b
add(5, 3)
Output:
Arguments: (5, 3) {}
Result: 8
Common Real-World Examples #
Decorators are widely used in real applications.
Authentication Decorator #
current_user = "Alice"
def require_login(func):
def wrapper(*args, **kwargs):
if current_user is None:
return "Please login first"
return func(*args, **kwargs)
return wrapper
@require_login
def view_profile():
return f"Profile of {current_user}"
print(view_profile())
This decorator ensures the user is authenticated before accessing the function.
Validation Decorator #
def validate_positive(func):
def wrapper(number):
if number <= 0:
raise ValueError("Number must be positive")
return func(number)
return wrapper
@validate_positive
def square_root(number):
return number ** 0.5
print(square_root(16))
This decorator validates input before the original function runs.
Caching Decorator #
def cache(func):
saved = {}
def wrapper(*args):
if args in saved:
return saved[args]
result = func(*args)
saved[args] = result
return result
return wrapper
@cache
def add(a, b):
print("Calculating...")
return a + b
print(add(2, 3))
print(add(2, 3))
The second function call returns the cached result instead of recalculating.
Decorators in JavaScript #
JavaScript implements the same concept using higher-order functions.
function timerDecorator(func) {
return function (...args) {
const start = Date.now();
const result = func(...args);
const end = Date.now();
console.log(`${func.name} took ${end - start} ms`);
return result;
};
}
Using it:
function fetchUserProfile() {
console.log("Fetching profile...");
}
fetchUserProfile = timerDecorator(fetchUserProfile);
fetchUserProfile();
The concept is identical:
original function -> wrapper -> enhanced function
Multiple Decorators #
Decorators can also be combined.
def uppercase(func):
def wrapper():
return func().upper()
return wrapper
def add_exclamation(func):
def wrapper():
return func() + "!"
return wrapper
@add_exclamation
@uppercase
def greet():
return "hello"
print(greet())
Output:
HELLO!
Decorator order matters because this runs as:
greet = add_exclamation(uppercase(greet))
The Hidden Problem #
Decorators introduce one small issue.
When a function is wrapped, Python replaces the original function with the wrapper function.
Example:
print(fetch_user_profile.__name__)
Output:
wrapper
This can create problems for:
- Debugging
- Documentation tools
- Frameworks that inspect functions
The Fix: functools.wraps #
Python provides wraps to preserve original function metadata.
from functools import wraps
def timer_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
Now:
print(fetch_user_profile.__name__)
Output:
fetch_user_profile
Using @wraps(func) is considered best practice when creating decorators.
Conclusion #
The Decorator Pattern is one of the most practical and widely used patterns in modern software development.
Whether you are using:
- Python decorators
- JavaScript higher-order functions
- Java annotations
- C# middleware and attributes
the core idea remains the same:
Add behavior around existing code without modifying the original implementation.
By separating additional responsibilities from business logic, decorators help create code that is cleaner, more reusable, and easier to maintain.