Friday, January 16, 2026
Clean Code Chapter 5: Formatting
Wednesday, January 7, 2026
Clean Code Chapter 4: Comments, or The Art of Saying Nothing
Clean Code Chapter 4: Comments, or The Art of Saying Nothing
One of the most counterintuitive lessons in Robert Martin's Clean Code appears in Chapter 4: the best comment is often no comment at all. As Martin puts it, "the proper use of comments is to compensate for our failure to express ourselves in code."
This doesn't mean comments are evil. It means they're a last resort. When you feel the urge to write a comment, your first instinct should be to ask: Can I refactor this code so the comment becomes unnecessary?
Why Comments Lie
Comments have a fundamental problem: they rot. Code changes, deadlines loom, and comments get left behind. Unlike code, comments don't throw errors when they become inaccurate. A misleading comment is worse than no comment at all because it actively deceives the reader.
Martin argues that we should spend our energy making code self-documenting rather than explaining unclear code with comments.
Good Comments vs. Bad Comments
Not all comments are bad. Martin identifies several legitimate uses:
Acceptable comments:
- Legal notices (copyright, licensing)
- Explanation of intent when the why isn't obvious from code
- Clarification of obscure arguments or return values from external libraries
- Warnings of consequences
- TODO markers (used sparingly)
Comments to avoid:
- Redundant comments that repeat what the code says
- Commented-out code (that's what version control is for)
- Position markers and banners
- Attributions (again, version control)
- Closing brace comments
Rails Example: Let the Code Speak
Here's a common pattern I see in Rails applications with over-commenting:
# Bad: Comments that add noise
class OrderProcessor
# Process the order
def process(order)
# Check if order is valid
return unless order.valid?
# Calculate the total price of all items
total = order.line_items.sum do |item|
# Multiply quantity by unit price
item.quantity * item.unit_price
end
# Apply discount if customer has one
if order.customer.discount_percentage.present?
# Calculate discount amount
discount = total * (order.customer.discount_percentage / 100.0)
# Subtract discount from total
total = total - discount
end
# Save the total to the order
order.update(total_amount: total)
# Send confirmation email to customer
OrderMailer.confirmation(order).deliver_later
end
end
Every comment here restates what the code already says. The method name process is vague, forcing us to read comments to understand what's happening. Let's refactor:
# Good: Self-documenting code
class OrderProcessor
def finalize_and_confirm(order)
return unless order.valid?
order.update(total_amount: calculate_discounted_total(order))
OrderMailer.confirmation(order).deliver_later
end
private
def calculate_discounted_total(order)
subtotal = order.line_items.sum(&:total_price)
apply_customer_discount(subtotal, order.customer)
end
def apply_customer_discount(amount, customer)
return amount unless customer.discount_percentage.present?
amount * (1 - customer.discount_percentage / 100.0)
end
end
The refactored version uses method names that explain intent. finalize_and_confirm tells you exactly what the public method does. calculate_discounted_total and apply_customer_discount make the business logic readable without a single comment.
Notice how extracting total_price to the LineItem model (implied by sum(&:total_price)) moves that logic where it belongs. The code reads like a sentence describing the business process.
React Example: When Comments Help and When They Hurt
Frontend code often accumulates comments explaining complex UI logic. Here's a React component drowning in unnecessary comments:
// Bad: Comment noise obscuring the code
interface CartProps {
items: CartItem[];
onCheckout: () => void;
}
function ShoppingCart({ items, onCheckout }: CartProps) {
// State for showing the discount input
const [showDiscount, setShowDiscount] = useState(false);
// State for the discount code value
const [discountCode, setDiscountCode] = useState('');
// State for discount validation error
const [error, setError] = useState<string | null>(null);
// Calculate the total price
const total = items.reduce((sum, item) => {
// Add item price times quantity to sum
return sum + item.price * item.quantity;
}, 0); // Start with 0
// Handle applying the discount code
const handleApplyDiscount = () => {
// Check if code is valid
if (discountCode.length < 5) {
// Set error message
setError('Invalid discount code');
return;
}
// Clear error and apply
setError(null);
// TODO: Actually apply discount
};
// Render the component
return (
<div className="cart">
{/* Map through items and display them */}
{items.map((item) => (
// Use item id as key
<CartItemRow key={item.id} item={item} />
))}
{/* Show total */}
<div className="total">Total: ${total.toFixed(2)}</div>
{/* Checkout button */}
<button onClick={onCheckout}>Checkout</button>
</div>
);
}
The comments here are pure noise. // Calculate the total price before a variable called total adds nothing. // Render the component before a return statement is almost comical. Let's clean this up:
// Good: Clear code with one meaningful comment
interface CartProps {
items: CartItem[];
onCheckout: () => void;
}
function ShoppingCart({ items, onCheckout }: CartProps) {
const [discountForm, setDiscountForm] = useState({
isVisible: false,
code: '',
error: null as string | null,
});
const cartTotal = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const applyDiscountCode = () => {
const MIN_CODE_LENGTH = 5;
if (discountForm.code.length < MIN_CODE_LENGTH) {
setDiscountForm((prev) => ({ ...prev, error: 'Invalid discount code' }));
return;
}
setDiscountForm((prev) => ({ ...prev, error: null }));
// TODO: Integrate with discount API once pricing service is deployed
};
return (
<div className="cart">
{items.map((item) => (
<CartItemRow key={item.id} item={item} />
))}
<div className="total">Total: ${cartTotal.toFixed(2)}</div>
<button onClick={onCheckout}>Checkout</button>
</div>
);
}
The refactored version groups related state into a single object with descriptive property names. cartTotal is more specific than total. The magic number 5 becomes a named constant MIN_CODE_LENGTH.
One comment remains: the TODO. This is a legitimate use because it explains why the code is incomplete (waiting on an external dependency) rather than restating what the code does.
The Litmus Test
Before writing a comment, ask yourself these questions:
- Can I rename this variable or function to eliminate the need for this comment?
- Can I extract this logic into a well-named method?
- Am I explaining what the code does (bad) or why it does something non-obvious (potentially acceptable)?
- Will this comment stay accurate as the code evolves?
If you can refactor instead of comment, do it. Your future self (and your teammates) will thank you.
The Takeaway
Comments aren't inherently bad, but they should make you pause. Each comment is an admission that your code couldn't speak for itself. Sometimes that's unavoidable. More often, it's an invitation to write clearer code.
As Martin writes: "The only truly good comment is the comment you found a way not to write."
Clean Code Chapter 5: Formatting
Clean Code Chapter 5: Formatting Code formatting might seem like a trivial concern compared to architecture or algorithms, but Robert C. M...
-
The Five Dysfunctions of Software Teams, Part 1: Absence of Trust This is the first post in a six-part series exploring The Five Dysfunctio...
-
If you've spent any time in software development, you've probably heard someone mention "Clean Code" by Robert C. Martin ...
-
Clean Code Chapter 5: Formatting Code formatting might seem like a trivial concern compared to architecture or algorithms, but Robert C. M...