When I was taking programming classes in college, comments were all the rage. Most of the solutions I handed in had one function (or very few), with comments above the class, above each function, within each function. Everywhere. Instructors wanted lots of comments.
In retrospect, I’m not sure why. Maybe so that if the code was off, they could still give partial credit, like asking students in a math class to “show their work”?
Since then, there’s been a corner of the developer world that prefers well-named functions to comments. In fact, they’d argue that well-named functions could replace comments altogether.
I’d take it further by saying that trying to create a good, representative name for a function, and failing, forces us to take a hard look at whether a function is really doing one thing well. And by refactoring large nebulous functions, that can eliminate the need for comments.
A Function to Collect Input
Let’s take a look at a relatively simple function that processes a banking transaction. (Other than the fact that it doesn’t actually record the transaction! Illustrative purposes only, folks.)
/// <summary> /// Prompt for account number, transaction type and amount. /// </summary> public void ProcessTransaction() { Console.Write("Account Number? "); string account = Console.ReadLine(); string type; do { Console.Write("[W]ithdrawal or [D]eposit? "); type = Console.ReadLine(); } while (type != "W" && type != "D"); Console.Write("Transaction Amount? "); decimal amount = Convert.ToDecimal(Console.ReadLine()); Console.WriteLine("Completing {0} for {1} in the amount of ${2}", (type == "W" ? "withdrawal" : "deposit"), account, amount); }
That’s simple enough. Query for a few pieces of info, just like the comments state.
Welp, that function’s as done as it’ll ever get…
A Function to Collect Input and Verify Account Holder
But over time, someone decides it’s insecure. I mean, we’re not even verifying the account holder!
We need to collect the user’s last name and birth date to verify who they are. (Pretend the last name and birth date are being pulled from a database somewhere based on the account number, and are not hard-coded… :p )
/// <summary> /// Prompt for account number, verify account holder, and process transaction. /// </summary> public void ProcessTransaction() { Console.Write("Account Number? "); string account = Console.ReadLine(); Console.Write("Verify your last name: "); string lastName = Console.ReadLine(); Console.Write("Verify your birth date (mm/dd/yyyy): "); string birthDate = Console.ReadLine(); if (lastName != "Jones" || birthDate != "07/04/1976") { Console.WriteLine("Unauthorized access! Call the troops! Storm the castle!!"); return; } string type; do { Console.Write("[W]ithdrawal or [D]eposit? "); type = Console.ReadLine(); } while (type != "W" && type != "D"); Console.Write("Transaction Amount? "); decimal amount = Convert.ToDecimal(Console.ReadLine()); Console.WriteLine("Completing {0} for {1} in the amount of ${2}", (type == "W" ? "withdrawal" : "deposit"), account, amount); }
Alright, the user’s identity has been flawlessly confirmed. Someone even remembered to update the comments.
All done!
A Function to Collect Input, Verify Account Holder, Email a Confirmation, Generate a Report, …
But oh wait. Wouldn’t it be nice to email them a confirmation of their transaction? And fire off an automated report somewhere. Everyone loves a good report. Ugh.
/// <summary> /// Prompt for account number, verify account holder, and process transaction. /// Send a confirmation email to the account holder (if requested). /// </summary> public void ProcessTransaction() { Console.Write("Account Number? "); string account = Console.ReadLine(); Console.Write("Verify your last name: "); string lastName = Console.ReadLine(); Console.Write("Verify your birth date (mm/dd/yyyy): "); string birthDate = Console.ReadLine(); if (lastName != "Jones" || birthDate != "07/04/1976") { Console.WriteLine("Unauthorized access! Call the troops! Storm the castle!!"); return; } string type; do { Console.Write("[W]ithdrawal or [D]eposit? "); type = Console.ReadLine(); } while (type != "W" && type != "D"); Console.Write("Transaction Amount? "); decimal amount = Convert.ToDecimal(Console.ReadLine()); Console.WriteLine("Completing {0} for {1} in the amount of ${2}", (type == "W" ? "withdrawal" : "deposit"), account, amount); Console.Write("Enter your email to receive a receipt of the transaction: "); string email = Console.ReadLine(); using (var client = new SmtpClient("some_host")) { client.Send("noreply@someservice.com", email, "Your confirmation email", string.Format("Receipt for your {0} for {1} in the amount of ${2}", (type == "W" ? "withdrawal" : "deposit"), account, amount)); } using (var rpt = new ReportDocument()) { rpt.Load("some/file.rpt"); rpt.SetParameterValue("account", account); rpt.SetParameterValue("type", type); rpt.SetParameterValue("amount", amount); rpt.ExportToDisk(ExportFormatType.PortableDocFormat, "some/other/location.pdf"); } }
Our function is quickly growing out of control, doing many many things.
At some point, someone is going to make a requirement that cannot simply be tacked on to the end of the function. It’s going to require some code at the beginning of the function, a few lines at the end, and more scattered throughout. Then more requirements come up, 6 months down the road. The logic is hard to follow, and adding features becomes a slog.
But at least the comments got updated after the email piece was added. Too bad whoever added the report forgot to update the comments. No problem though, because comments are second-class citizens, so to speak.
Change comments as you please, forget to update them, delete them entirely. Your code still compiles.
What if we instead change the name of the function to match the comments?
/// <summary> /// Prompt for account number, verify account holder, and process transaction. /// Send a confirmation email to the account holder (if requested). /// </summary> public void VerifyAccountHolderAndProcessTransactionAndSendConfirmationEmail() { Console.Write("Account Number? "); string account = Console.ReadLine(); Console.Write("Verify your last name: "); string lastName = Console.ReadLine(); Console.Write("Verify your birth date (mm/dd/yyyy): "); string birthDate = Console.ReadLine(); ... ...
That’s ridiculous. We’d have to refactor our code every time we added new functionality.
The larger problem here is that this function is doing way too much and needs to be broken up. Each function should do one thing, or have one purpose, in a nod to the single responsibility principle (usually applied to entire classes, but it’s useful to reference it here too).
Identify Distinct Purposes in Large Functions, and Refactor
I spent about 15 minutes refactoring the above code into separate, well-named functions. Specifically, I found the following opportunities:
- Pull out related bits of logic into separate functions (sending an email or generating a report)
- Name the functions according to the (hopefully) single thing they do
- Group details the account holder enters into a single class, which we can then pass around instead of independent variables
- There’s repetition in prompting the user with a message, collecting their input, and then converting the input string to the necessary type. That logic can be pulled out into a separate function that uses generics (see QueryAccountHolderForData below).
Check out the ProcessTransaction function now. Can you get a pretty good idea of what it does from the names of the functions it calls, before you even delve into those other functions?
public void ProcessTransaction() { var transaction = new Transaction(); transaction.AccountNumber = QueryAccountHolderForData<string>("Account Number? "); if (!IsAccountHolderIdentityConfirmedThroughSecurityAudit(transaction.AccountNumber)) return; transaction.Type = QueryAccountHolderForData<string>("[W]ithdrawal or [D]eposit? ", "W", "D") transaction.Amount = QueryAccountHolderForData<decimal>("Transaction Amount? "); ConfirmTransactionOnScreenToAccountHolder(transaction); SendAccountHolderConfirmationEmailForTransaction(transaction); GenerateReportForSingleTransaction(transaction); } private bool IsAccountHolderIdentityConfirmedThroughSecurityAudit(string accountNumber) { // TODO: Lookup the customer details, security questions, etc in a database var lastName = QueryAccountHolderForData<string>("Verify your last name: "); var birthDate = QueryAccountHolderForData<DateTime>("Verify your birth date (mm/dd/yyyy): "); if (lastName != "Jones" || birthDate != new DateTime(1976, 7, 4)) { Console.WriteLine("Unauthorized access! Call the troops! Storm the castle!!"); return false; } return true; } private void ConfirmTransactionOnScreenToAccountHolder(Transaction t) { Console.WriteLine("Completing {0} for {1} in the amount of ${2}", GetTransactionDescriptionFromType(t.Type), t.AccountNumber, t.Amount); } private void SendAccountHolderConfirmationEmailForTransaction(Transaction t) { Console.Write("Enter your email to receive a receipt of the transaction: "); string email = Console.ReadLine(); using (var client = new SmtpClient("some_host")) { client.Send("noreply@someservice.com", email, "Your confirmation email", string.Format("Receipt for your {0} for {1} in the amount of ${2}", GetTransactionDescriptionFromType(t.Type), t.AccountNumber, t.Amount)); } } private void GenerateReportForSingleTransaction(Transaction t) { using (var rpt = new ReportDocument()) { rpt.Load("some/file.rpt"); rpt.SetParameterValue("account", t.AccountNumber); rpt.SetParameterValue("type", t.Type); rpt.SetParameterValue("amount", t.Amount); rpt.ExportToDisk(ExportFormatType.PortableDocFormat, "some/other/location.pdf"); } } private string GetTransactionDescriptionFromType(string transactionType) { return (transactionType == "W") ? "withdrawal" : "deposit"; } private T QueryAccountHolderForData<T>(string message) { Console.Write(message); return (T)Convert.ChangeType(Console.ReadLine(), typeof(T)); } private T QueryAccountHolderForData<T>(string message, params T[] allowedValues) { T returnValue; do { returnValue = QueryAccountHolderForData<T>(message); } while (!allowedValues.Contains(returnValue)); return returnValue; } private class Transaction { public string AccountNumber { get; set; } public string Type { get; set; } public decimal Amount { get; set; } }
Arguably, there’s still room for improvement. Perhaps GetTransactionDescriptionFromType belongs inside the Transaction class. Maybe transaction type could be made into an enumeration, if that’s helpful.
Overall though, it’s far easier to look at the ProcessTransaction function and follow what it’s doing. It basically reads like English, and then you can delve into the functions to see what one thing (or very few things) each is doing. BTW, if you’re new to generics they’re pretty awesome – if you don’t understand what it’s doing, feel free to leave a question in the comments section below.
Comments Aren’t Evil!
Don’t throw out comments altogether. There are always cases where some esoteric piece of code will beg a further explanation. Or you’ll want to reference a URL with documentation on a bug that led to a piece of code that seems otherwise unnecessary.
If everyone on the team stays on top of them over the years, then you can run tools that will generate documentation from them. An IDE like Visual Studio may even incorporate your comments into its intellisense to hint to other developers what the function does when they hover over it in the autocompletion context menu.
The code can go a long way towards documenting itself, and comments can always be added if the team agrees there are benefits to them.







