In this tutorial, we will give you nine practical techniques for writing elegant and readable code. We won't be talking about specific architectures, languages, or platforms. The focus lies on writing better code. Let's get started.
"Measuring programming progress by lines of code is like measuring aircraft building progress by weight." - Bill Gates
Introduction
If you're a developer, then there probably have been times when you've written code and, after a few days, weeks, or months, you looked back at it and said to yourself "What does this piece of code do?" The answer to that question might have been "I really don't know!" In that case, the only thing you can do is going through the code from start to finish, trying to understand what you were thinking when you wrote it.
This mostly happens when we're lazy and we just want to implement that new feature the client asked for. We just want to get the job done with as little effort as possible. And when it works, we don't care about the code itself, because the client won't ever see the ugly truth, let alone understand it. Right? Wrong. These days, collaborating on software has become the default and people will see, read, and inspect the code that you write. Even if your code isn't scrutinized by your colleagues, you should make it a habit to write clear and readable code. Always.
Most of the time, you don't work alone on a project. We frequently see ugly code with variables having names like i
, a
, p
, pro
, and rqs
. And if it really gets bad, this pattern is visible across the entire project. If this sounds familiar, then I'm pretty sure you've asked yourself the question, "How can this person write code like this?" Of course, this makes you all the more grateful when you come across clear, readable, and even beautiful code. Clear and clean code can be read in seconds and it can save you and your colleagues a lot of time. That should be your motivation for writing quality code.
1. Easy to Understand
We all agree that code should be easy to understand. Right? The first example focuses on spacing. Let's look at two examples.
return gender == "1" ? weight * (height / 10) : weight * (height * 10);
if(gender == "1"){ return weight * (height / 10); } else { return weight * (height * 10); }
Even though the result of theses examples is identical, they look quite different. Why should you use more lines of code if you can write less? Let's explore two other examples, something I bet you see frequently.
for (Node* node = list->head; node != NULL; node = node->next) print(node->data);
Node* node = list->head; if(node == NULL) return; while(node->next != NULL) { Print(node->data); node = node->next; } if(node != NULL) Print(node->data);
Again, the result of these examples is identical. Which one's better? And why? Do fewer lines of code mean better code? We'll revisit this question later in this tutorial.
2. Is Smaller Always Better?
In computer science, you often hear the phrase "less is more". Generally speaking, if you can solve a problem in fewer lines of code, the better. It will probably take you less time to understand a 200-line class than a 500-line class. However, is this always true? Take a look at the following examples.
reservation((!room = FindRoom(room_id))) || !room->isOccupied());
room = FindRoom(room_id); if(room != NULL) reservation(!room->isOccupied());
Don't you agree that the second example is easier to read and understand? You need to be able to optimize for readability. Of course, you could add a few comments to the first example to make it easier to understand, but isn't it better to omit the comments and write code that's easier to read and understand?
// Determine where to spawn the monster along the Y axis CGSize winSize = [CCDirector sharedDirector].winSize; int minY = monster.contentSize.width / 2; int maxY = winSize.width - monster.contentSize.width/2; int rangeY = maxY - minY; int actualY = (arc4random() % rangeY) + minY;
3. Naming
Choosing descriptive names for things like variables and functions is a key aspect of writing readable code. It helps both your colleagues and yourself to quickly understand the code. Naming a variable tmp
doesn't tell you anything other than that the variable is temporary for some reason, which is nothing more than an educated guess. It doesn't tell if the variable stores a name, a date, etc.
Another fine example is naming a method stop
. It's not a bad name per se, but that really depends on the method's implementation. If it performs a dangerous operation that cannot be undone, you may want to rename it to kill
or pause
if the operation can be resumed. Do you get the idea?
If you're working with a variable for the weight of potatoes, why would you name it tmp
? When you revisit that piece of code a few days later, you won't recall what tmp
is used for.
We're not saying that tmp
is a bad name for a variable, because there are times when tmp
is perfectly reasonable as a variable name. Take a look at the following example in which tmp
isn't a bad choice at all.
tmp = first_potato; first_potato = second_potato; second_potato = tmp;
In the above example, tmp
describes what it does, it temporarily stores a value. It isn't passed to a function or method, and it isn't incremented or modified. It has a well-defined lifetime and no experienced developer will be thrown off by the variable's name. Sometimes, however, it's just plain laziness. Take a look at the next example.
NSString *tmp = user.name; tmp += " " + user.phone_number; tmp += " " + user.email; ... [template setObject:tmp forKey:@"user_info"];
If tmp
stores the user's information, why isn't it named userInfo
? Proper naming of variables, functions, methods, classes, etc. is important when writing readable code. Not only does it make your code more readable, it will save you time in the future.
4. Add Meaning to Names
As we saw in the previous tip, it's important to choose names wisely. However, it's equally important to add meaning to the names you use for variables, functions, methods, etc. Not only does this help avoid confusion, it makes the code you write easier to understand. Choosing a name that makes sense is almost like adding metadata to a variable or method. Choose descriptive names and avoid generic ones. The word add
, for example, isn't always ideal as you can see in the next example.
bool addUser(User u){ ... }
It's not clear what addUser
is supposed to do. Does it add a user to a list of users, to a database, or to a list of people invited to a party? Compare this to registerUser
or signupUser
. This makes more sense. Right? Take a look at the following listing to get a better idea of what we're driving at.
Word | Synonyms | do | make, perform, execute, compose, add | start | launch, create, begin, open | explode | detonate, blow up, set off, burst |
5. Name Size
A lot of programmers don't like long names, because they're hard to remember and cumbersome to type. Of course, a name shouldn't be ridiculously long like newClassForNavigationControllerNamedFirstViewController
. This is hard to remember and it simply makes your code ugly and unreadable.
As we saw earlier, the opposite, short names, aren't any good either. What is the right size for a variable or method name? How do you decide between naming a variable len
, length
, or user_name_length
? The answer depends on the context and the entity to which the name is tied to.
Long names are no longer a problem when using a modern IDE (Integrated Development Environment). Code completion helps you avoid typos and it also makes suggestions to make remembering names less of a pain.
You can use short(er) names if the variable is local. What's more, it's recommended to use shorter names for local variables to keep your code readable. Take a look at the following example.
NSString *link = [[NSString alloc] initWithFormat:@"http://localhost:8080/WrittingGoodCode/resources/GoodCode/getGoodCode/%@",idCode]; NSURL *infoCode = [NSURL URLWithString:link];
6. Naming Booleans
Booleans can be tricky to name, because they can have a different meaning depending on the way you read or interpret the name. In the next code snippet, read_password
can mean that the password has been read by the program, but it can also mean that the program should read the password.
BOOL readPassword = YES;
To avoid this problem, you could rename the above boolean to didReadPassword
to indicate that the password has been read or shouldReadPassword
to show that the program needs to read the password. This is something you see a lot in Objective-C, for example.
7. To Comment or Not To Comment
Adding comments to code is important, but it's equally important to use them sparingly. They should be used to help someone understand your code. Reading comments, however, takes time too and if a comment doesn't add much value, then that time is wasted. The next code snippet shows how not to use comments.
// This happens when memory warning is received - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. } // This validate the fields -(BOOL)validateFields { }
Are these code snippets helpful to you? The answer is probably "No." The comments in the above examples do not add additional information, especially since the method names are already very descriptive, which is common in Objective-C. Don't add comments that explain the obvious. Take a look at the next example. Isn't this a much better use of comments?
// Determine speed of the monster int minDuration = 2.0; int maxDuration = 8.0; int rangeDuration = maxDuration - minDuration; int actualDuration = (arc4random() % rangeDuration) + minDuration;
Comments like this make it very easy to navigate a code base quickly and efficiently. It saves you from having to read the code and helps you understand the logic or algorithm.
8. Style and Consistency
Every language or platform has a (or more) style guide and even most companies have one. Do you put the curly braces of an Objective-C method on a separate line or not?
- (void)calculateOffset { }
- (void)calculateOffset { }
The answer is that it doesn't matter. There's no right answer. Of course, there are style guides you can adopt. What's important is that your code is consistent in terms of style. Even though this may not affect the quality of your code, it certainly affects the readability and it will most likely annoy the hell out of your colleagues or whoever reads your code. For most developers, ugly code is the worst kind of code.
9. Focused Methods and Functions
A common mistake among developers is trying to cram as much functionality into functions and methods. This works, but it's inelegant and makes debugging a pain in the neck. Your life—and that of your colleagues—will become much easier if you break larger problems into tiny bits and tackle those bits into separate functions or methods. Take a look at the next example in which we write an image to disk. This seems like a trivial task, but there's a lot more to it if you want to do it right.
- (BOOL)saveToImage:(UIImage *)image withFileName:(NSString *)fileName { BOOL result = NO; NSString *documents = nil; NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); if (paths.count) { documents = [paths objectAtIndex:0]; NSString *basePath = [documents stringByAppendingPathComponent:@"Archive"]; if (![[NSFileManager defaultManager] fileExistsAtPath:basePath]) { NSError *error = nil; [[NSFileManager defaultManager] createDirectoryAtPath:basePath withIntermediateDirectories:YES attributes:nil error:&error]; if (!error) { NSString *filePath = [basePath stringByAppendingPathComponent:fileName]; result = [UIImageJPEGRepresentation(image, 8.0) writeToFile:filePath atomically:YES]; } else { NSLog(@"Unable to create directory due to error %@ with user info %@.", error, error.userInfo); } } } return result; }
If a unit of code tries to do too much, you often end up with deeply nested conditional statements, a lot of error checking, and overly complex conditional statements. This method does three things, fetch the path of the application's documents directory, fetch and create the path for the archives directory, and write the image to disk. Each task can be put in its own method as shown below.
- (BOOL)saveToImage:(UIImage *)image withFileName:(NSString *)fileName { NSString *archivesDirectory = [self applicationArchivesDirectory]; if (!archivesDirectory) return NO; // Create Path NSString *filePath = [archivesDirectory stringByAppendingPathComponent:fileName]; // Write Image to Disk return [UIImageJPEGRepresentation(image, 8.0) writeToFile:filePath atomically:YES]; }
- (NSString *)applicationDocumentsDirectory { NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); return paths.count ? [paths objectAtIndex:0] : nil; }
- (NSString *)applicationArchivesDirectory { NSString *documentsDirectory = [self applicationDocumentsDirectory]; NSString *archivesDirectory = [documentsDirectory stringByAppendingPathComponent:@"Archives"]; NSFileManager *fm = [NSFileManager defaultManager]; if (![fm fileExistsAtPath:archivesDirectory]) { NSError *error = nil; [fm createDirectoryAtPath:archivesDirectory withIntermediateDirectories:YES attributes:nil error:&error]; if (error) { NSLog(@"Unable to create directory due to error %@ with user info %@.", error, error.userInfo); return nil; } } return archivesDirectory; }
This is much easier to debug and maintain. You can even reuse the applicationDocumentsDirectory
method in other places of the project, which is another benefit of breaking larger problems into manageable pieces. Testing code becomes much easier too.
Conclusion
In this article, we've taken a closer look at writing readable code by wisely choosing names for variables, functions, and methods, being consistent when writing code, and breaking complex problems into manageable chunks. If you have any questions or feedback, feel free to leave a comment below.