Comments and Storytelling
Recently I have stumbled upon this discussion between Uncle Bob and John Ousterhout. It’s a bit aggressive and harsh, but nonetheless it’s worth reading! They go through 3 main topics:
- Refactoring and small methods vs longer methods.
- Comments, useful or not?
- TDD.
I want to discuss the second and third topics, but in this post I’ll focus on the second one: Comments!
My opinion is that comments should give a piece of history or story around why a decision was made. I don’t think a minimum amount of comments and self-documented code is always sufficient, though most of the time it is! But sometimes there is a missing piece about why the decision was made, and even if the code is quite self-explanatory, the “why” cannot be guessed.
For example, let’s look at this fake authentication method:
class User < ApplicationRecord
def authenticate(password)
# We don't use has_secure_password here because we needed to maintain
# compatibility with our legacy system that used a custom hashing algorithm.
# This implementation ensures that both old and new passwords work during
# the transition period. See migration plan in JIRA ticket AUTH-4592.
return false if password_digest.blank?
if password_digest.start_with?('legacy:')
# Legacy algorithm with different salt approach
legacy_digest = password_digest.sub('legacy:', '')
LegacyPasswordService.check(password, legacy_digest).tap do |result|
# Upgrade to bcrypt if login successful with legacy password
update(password: password) if result
end
else
BCrypt::Password.new(password_digest).is_password?(password)
end
end
end
The comment here tells us exactly why we’re not using Rails’ built-in has_secure_password. Without this comment, someone might think “let’s refactor this to use the standard approach” and break compatibility with the legacy system. The comment gives us crucial insight that we’d never get just from the code!
Here’s another example on a possible payment system:
class PaymentProcessor
def process_international_payment(amount, currency, recipient)
# Japanese yen amounts must be handled as whole numbers without decimal places
# due to requirements from their banking API. One JPY in our system is sent as 1,
# unlike other currencies where we send the amount in smallest unit (cents)
if currency.upcase == 'JPY'
formatted_amount = amount.to_i
else
# Convert from dollars to cents for other currencies
formatted_amount = (amount * 100).to_i
end
# Stripe has a 25-second timeout for payments to Turkey (see outage report 2023-05-12)
# so we increase our timeout and add retries only for this specific country
timeout = recipient.country_code == 'TR' ? 45 : 30
api_client.create_payment(
amount: formatted_amount,
currency: currency,
recipient: recipient,
request_timeout: timeout
)
end
end
The comments explain business logic that isn’t obvious at all from the code. Without them, someone might try to “clean up” the code by standardizing the amount formatting or timeouts, and boom! You’ve got subtle bugs in production that are super hard to track down.
And if a new developer decides to refactor the code, which they should consider, they might think “why not use a Money gem to handle the details?” With these comments, they will have extra information to actually perform the best changes, and not just wait for a test to break or a fire to be put out.
Now look at this example, explaining exactly what a code should do, in steps where the code itself can already express these actions:
class Order < ApplicationRecord
# This method calculates the total price of the order
def calculate_total
# Initialize the total variable to zero
total = 0
# Loop through each item in the order
order_items.each do |item|
# Get the price of each item
item_price = item.price
# Get the quantity of each item
quantity = item.quantity
# Multiply price by quantity to get item subtotal
item_subtotal = item_price * quantity
# Add the item subtotal to the total
total += item_subtotal
end
# If there is a discount, subtract it from the total
if discount.present?
# Get the discount amount
discount_amount = discount.amount
# Subtract the discount from the total
total -= discount_amount
end
# Return the final total
return total
end
# This method checks if an order can be shipped
def ready_to_ship?
# Check if payment has been processed
payment_processed = payment.completed?
# Check if all items are in stock
items_available = order_items.all? { |item| item.in_stock? }
# Return true only if payment is processed and items are available
return payment_processed && items_available
end
end
These comments in the last example do not help at all. This is especially true with Ruby, an eloquent language. Why explain if something is in stock when Ruby itself can express this clearly with item.in_stock??
These examples show exactly what I’m talking about — comments that go beyond just describing the code. They give us the backstory, the why behind decisions that we could never guess from the code alone. They reference things like historical constraints, API quirks, and business rules that aren’t apparent in the code itself. That’s the real value of good comments!
Now, comments should give context and answer questions that code cannot do. Explain the whys. Otherwise, your code should always be self-explanatory. This includes using good class names and methods that follow good principles and have good design. So you won’t over-engineer anything or add hundreds of bad comments. Frameworks (such as Rails) have great examples of comments that add context, explanations and lead you to a correct solution.
Happy coding!
Recentemente encontrei esta discussão entre Uncle Bob e John Ousterhout. Ela é um pouco agressiva e dura, mas ainda assim vale a leitura! Eles passam por 3 tópicos principais:
- Refatoração e métodos pequenos vs métodos mais longos.
- Comentários: úteis ou não?
- TDD.
Quero discutir o segundo e o terceiro tópicos, mas neste post vou focar no segundo: Comentários!
Minha opinião é que comentários devem dar um pedaço de história ou narrativa sobre por que uma decisão foi tomada. Não acho que uma quantidade mínima de comentários e código autodocumentado seja sempre suficiente, embora na maior parte do tempo seja! Mas às vezes existe uma peça faltando sobre por que a decisão foi tomada e, mesmo que o código seja bastante autoexplicativo, o “porquê” não pode ser adivinhado.
Por exemplo, vamos olhar para este método falso de autenticação:
class User < ApplicationRecord
def authenticate(password)
# We don't use has_secure_password here because we needed to maintain
# compatibility with our legacy system that used a custom hashing algorithm.
# This implementation ensures that both old and new passwords work during
# the transition period. See migration plan in JIRA ticket AUTH-4592.
return false if password_digest.blank?
if password_digest.start_with?('legacy:')
# Legacy algorithm with different salt approach
legacy_digest = password_digest.sub('legacy:', '')
LegacyPasswordService.check(password, legacy_digest).tap do |result|
# Upgrade to bcrypt if login successful with legacy password
update(password: password) if result
end
else
BCrypt::Password.new(password_digest).is_password?(password)
end
end
end
O comentário aqui nos diz exatamente por que não estamos usando o has_secure_password embutido do Rails. Sem esse comentário, alguém poderia pensar “vamos refatorar isso para usar a abordagem padrão” e quebrar a compatibilidade com o sistema legado. O comentário nos dá um insight crucial que nunca obteríamos apenas pelo código!
Aqui vai outro exemplo de um possível sistema de pagamentos:
class PaymentProcessor
def process_international_payment(amount, currency, recipient)
# Japanese yen amounts must be handled as whole numbers without decimal places
# due to requirements from their banking API. One JPY in our system is sent as 1,
# unlike other currencies where we send the amount in smallest unit (cents)
if currency.upcase == 'JPY'
formatted_amount = amount.to_i
else
# Convert from dollars to cents for other currencies
formatted_amount = (amount * 100).to_i
end
# Stripe has a 25-second timeout for payments to Turkey (see outage report 2023-05-12)
# so we increase our timeout and add retries only for this specific country
timeout = recipient.country_code == 'TR' ? 45 : 30
api_client.create_payment(
amount: formatted_amount,
currency: currency,
recipient: recipient,
request_timeout: timeout
)
end
end
Os comentários explicam regras de negócio que não são nada óbvias pelo código. Sem eles, alguém poderia tentar “limpar” o código padronizando a formatação dos valores ou os timeouts, e pronto! Você ganhou bugs sutis em produção que são super difíceis de rastrear.
E se um novo desenvolvedor decidir refatorar o código, o que ele deveria considerar, talvez pense: “por que não usar uma gem Money para lidar com os detalhes?” Com esses comentários, ele terá informação extra para realmente fazer as melhores mudanças, e não apenas esperar um teste quebrar ou um incêndio precisar ser apagado.
Agora olhe este exemplo, explicando exatamente o que um código deve fazer, em passos nos quais o próprio código já consegue expressar essas ações:
class Order < ApplicationRecord
# This method calculates the total price of the order
def calculate_total
# Initialize the total variable to zero
total = 0
# Loop through each item in the order
order_items.each do |item|
# Get the price of each item
item_price = item.price
# Get the quantity of each item
quantity = item.quantity
# Multiply price by quantity to get item subtotal
item_subtotal = item_price * quantity
# Add the item subtotal to the total
total += item_subtotal
end
# If there is a discount, subtract it from the total
if discount.present?
# Get the discount amount
discount_amount = discount.amount
# Subtract the discount from the total
total -= discount_amount
end
# Return the final total
return total
end
# This method checks if an order can be shipped
def ready_to_ship?
# Check if payment has been processed
payment_processed = payment.completed?
# Check if all items are in stock
items_available = order_items.all? { |item| item.in_stock? }
# Return true only if payment is processed and items are available
return payment_processed && items_available
end
end
Esses comentários no último exemplo não ajudam em nada. Isso é especialmente verdadeiro com Ruby, uma linguagem eloquente. Por que explicar se algo está em estoque quando o próprio Ruby consegue expressar isso claramente com item.in_stock??
Esses exemplos mostram exatamente do que estou falando — comentários que vão além de apenas descrever o código. Eles nos dão a história por trás, o porquê das decisões que jamais conseguiríamos adivinhar apenas pelo código. Eles referenciam coisas como restrições históricas, peculiaridades de APIs e regras de negócio que não estão aparentes no código em si. Esse é o valor real de bons comentários!
Agora, comentários devem dar contexto e responder perguntas que o código não consegue responder. Explique os porquês. Caso contrário, seu código deve sempre ser autoexplicativo. Isso inclui usar bons nomes de classes e métodos que sigam bons princípios e tenham bom design. Assim você não vai superengenheirar nada nem adicionar centenas de comentários ruins. Frameworks (como Rails) têm ótimos exemplos de comentários que adicionam contexto, explicações e levam você a uma solução correta.
Happy coding!