How to Write Better Functions: A Clean Code Checklist
Functions are the building blocks of your code - they break down complex operations into manageable, reusable pieces. Well-designed functions are key to creating systems that are easy to understand, test, and maintain over time. This post presents four key checkpoints for reviewing functions, inspired by the book Clean Code. Each checkpoint includes practical examples showing common problems and their solutions. Function Review Checklist Is the function small, focused, and does its name match what it does? Does the function have too many arguments? Are errors handled with exceptions instead of error codes? Is there any unnecessary duplication? Checkpoint 1: Is the function small, focused, and does its name match what it does? A function should do one thing and do it well. Here are three key signs of a focused function: a. It works at one level of abstraction Problem: High-level logic mixed with low-level logic void processOrder(Order order) { if (!order.hasValidPayment()) { throw new InvalidOrderException("Payment validation failed"); } // Low-level database operation mixed with high-level logic orderRepository.executeSQL("INSERT INTO orders VALUES (" + order.getId() + ", ...)"); } Solution: Consistent abstraction level void processOrder(Order order) { validateOrder(order); saveOrder(order); } b. It either does something (command) or answers something (query), but not both Problem: Mixed command and query int incrementAndGet() { count++; return count; } Solution: Separate command and query void increment() { count++; } int getCount() { return count; } c. It doesn't have hidden side effects Problem: Hidden side effect double calculateTotal(Order order) { if (order.getTotal() > 100) { order.applyDiscount(10); // Surprising! Calculating total but modifying order } return order.getTotal(); } Solution: No side effects double calculateTotal(Order order) { return order.getTotal(); } void applyDiscountIfEligible(Order order) { if (calculateTotal(order) > 100) { order.applyDiscount(10); } } Checkpoint 2: Does the function have too many arguments? Functions should have as few arguments as possible. Functions with too many arguments are hard to read, use, and test. Here are common solutions to handle multiple parameters effectively: a. Group related parameters into an object Problem: Too many related arguments void createUser(String name, String email, String phone, String address) { // User creation logic } Solution: Group related data into an object void createUser(UserProfile profile) { // User creation logic } b. Use builder pattern for optional parameters Problem: Multiple constructor variations class Report { Report(String title) { ... } Report(String title, Date from) { ... } Report(String title, Date from, Date to) { ... } } Solution: Builder pattern for clear, flexible construction Report report = new ReportBuilder() .setTitle("Sales Report") .setFromDate(startDate) // Optional .setToDate(endDate) // Optional .build(); Checkpoint 3: Are errors handled with exceptions instead of error codes? Error codes mix error handling with normal flow and are easy to ignore. Exceptions make error cases explicit and separate them from the main logic. Example: a. Use exceptions instead of error codes Problem: Using error codes int withdraw(Account account, double amount) { if (amount account.balance) return -2; // Insufficient funds account.balance -= amount; return 0; // Success } Solution: Using specific exceptions void withdraw(Account account, double amount) { if (amount account.balance) { throw new InsufficientFundsException("Not enough balance"); } account.balance -= amount; } Checkpoint 4: Is there any unnecessary duplication? Duplicated code increases the risk of bugs and makes changes harder. Here's how to identify and eliminate common types of duplication: a. Extract repeated validation logic Problem: Duplicated validation class UserService { void createUser(String email) { if (email == null || !email.contains("@")) { throw new InvalidEmailException(); } // Create user } void updateEmail(String email) { if (email == null || !email.contains("@")) { throw new InvalidEmailException(); } // Update email } } Solution: Single validation function class UserService { private void validateEmail(String email) { if (email == null || !email.contains("@")) { throw new InvalidEmailException(); } } void createUser(String email) { validateEmail(email); // Create

Functions are the building blocks of your code - they break down complex operations into manageable, reusable pieces. Well-designed functions are key to creating systems that are easy to understand, test, and maintain over time.
This post presents four key checkpoints for reviewing functions, inspired by the book Clean Code. Each checkpoint includes practical examples showing common problems and their solutions.
Function Review Checklist
- Is the function small, focused, and does its name match what it does?
- Does the function have too many arguments?
- Are errors handled with exceptions instead of error codes?
- Is there any unnecessary duplication?
Checkpoint 1: Is the function small, focused, and does its name match what it does?
A function should do one thing and do it well. Here are three key signs of a focused function:
a. It works at one level of abstraction
Problem: High-level logic mixed with low-level logic
void processOrder(Order order) {
if (!order.hasValidPayment()) {
throw new InvalidOrderException("Payment validation failed");
}
// Low-level database operation mixed with high-level logic
orderRepository.executeSQL("INSERT INTO orders VALUES (" + order.getId() + ", ...)");
}
Solution: Consistent abstraction level
void processOrder(Order order) {
validateOrder(order);
saveOrder(order);
}
b. It either does something (command) or answers something (query), but not both
Problem: Mixed command and query
int incrementAndGet() {
count++;
return count;
}
Solution: Separate command and query
void increment() {
count++;
}
int getCount() {
return count;
}
c. It doesn't have hidden side effects
Problem: Hidden side effect
double calculateTotal(Order order) {
if (order.getTotal() > 100) {
order.applyDiscount(10); // Surprising! Calculating total but modifying order
}
return order.getTotal();
}
Solution: No side effects
double calculateTotal(Order order) {
return order.getTotal();
}
void applyDiscountIfEligible(Order order) {
if (calculateTotal(order) > 100) {
order.applyDiscount(10);
}
}
Checkpoint 2: Does the function have too many arguments?
Functions should have as few arguments as possible. Functions with too many arguments are hard to read, use, and test.
Here are common solutions to handle multiple parameters effectively:
a. Group related parameters into an object
Problem: Too many related arguments
void createUser(String name, String email, String phone, String address) {
// User creation logic
}
Solution: Group related data into an object
void createUser(UserProfile profile) {
// User creation logic
}
b. Use builder pattern for optional parameters
Problem: Multiple constructor variations
class Report {
Report(String title) { ... }
Report(String title, Date from) { ... }
Report(String title, Date from, Date to) { ... }
}
Solution: Builder pattern for clear, flexible construction
Report report = new ReportBuilder()
.setTitle("Sales Report")
.setFromDate(startDate) // Optional
.setToDate(endDate) // Optional
.build();
Checkpoint 3: Are errors handled with exceptions instead of error codes?
Error codes mix error handling with normal flow and are easy to ignore. Exceptions make error cases explicit and separate them from the main logic. Example:
a. Use exceptions instead of error codes
Problem: Using error codes
int withdraw(Account account, double amount) {
if (amount <= 0) return -1; // Invalid amount
if (amount > account.balance) return -2; // Insufficient funds
account.balance -= amount;
return 0; // Success
}
Solution: Using specific exceptions
void withdraw(Account account, double amount) {
if (amount <= 0) {
throw new InvalidAmountException("Amount must be positive");
}
if (amount > account.balance) {
throw new InsufficientFundsException("Not enough balance");
}
account.balance -= amount;
}
Checkpoint 4: Is there any unnecessary duplication?
Duplicated code increases the risk of bugs and makes changes harder. Here's how to identify and eliminate common types of duplication:
a. Extract repeated validation logic
Problem: Duplicated validation
class UserService {
void createUser(String email) {
if (email == null || !email.contains("@")) {
throw new InvalidEmailException();
}
// Create user
}
void updateEmail(String email) {
if (email == null || !email.contains("@")) {
throw new InvalidEmailException();
}
// Update email
}
}
Solution: Single validation function
class UserService {
private void validateEmail(String email) {
if (email == null || !email.contains("@")) {
throw new InvalidEmailException();
}
}
void createUser(String email) {
validateEmail(email);
// Create user
}
void updateEmail(String email) {
validateEmail(email);
// Update email
}
}
b. Create utility functions for common operations
Problem: Repeated string manipulation
String name1 = input1.trim().toLowerCase().replace(" ", "");
String name2 = input2.trim().toLowerCase().replace(" ", "");
Solution: Utility function
String standardizeName(String input) {
return input.trim().toLowerCase().replace(" ", "");
}
Wrap-up
Well-designed functions make code easier to understand, test, and maintain. When reviewing code, check for:
- Single responsibility with appropriate abstraction levels
- Minimal, well-organized parameters
- Clear error handling with exceptions
- No unnecessary duplication
These improvements might seem small, but they compound to create significantly more maintainable systems.