pointer-image   25   Program Intently and Expressively

 

“Code that works and is understandable is nice, but it’s more important to be clever. You’re paid for being smart; show us how good you are.”

images/devil.png
Hoare on Software Design
by C.A.R. Hoare
C.A.R. Hoare

There are two ways of creating a software design. One way is to make it so simple that there are obviously no deficiencies. And the other way is to make it so complicated that there are no obvious deficiencies.

You’ve probably seen a lot of code that’s hard to understand, that’s hard to maintain, and (worst of all) has errors. You can tell code is bad when a group of developers circle around it like spectators near a UFO—with the same mix of apprehension, confusion, and helplessness. What good is a piece of code if no one can understand how it works?

When developing code, you should always choose readability over convenience. Code will be read many, many more times than it is written; it’s well worth it to take a small performance hit during writing if it makes the reading easier. In fact, code clarity comes before execution performances as well.

For instance, if default or optional arguments are going to make your code less readable, less understandable, or buggier, it would be better to specify the arguments explicitly, rather than cause later confusion.

When you modify a piece of code to fix a bug or add new feature, try to approach it systematically. First, you have to understand what the code does and how it works. Then, you need to figure out what you’re going to change. You then make your changes and test. The first of these steps, understanding the code, is often the hardest. If someone hands you code that’s easy to understand, they’re making your life a lot easier. Honoring the Golden Rule, you owe it to them to make your own code easy to read.

One way to make code understandable is to make it obvious to see what’s happening. Let’s look at some examples:

 
coffeeShop.PlaceOrder(2);

Reading the above code, you can probably figure out that we’re placing an order at a coffee shop. But, what in the world is 2? Does that mean two cups of coffee? Two shots? Or is it the size of the cup? The only way for you to be certain is to look at the method definition or the documentation. This code isn’t easy to understand by reading.

So we add some comments to make the code easier to understand:

 
coffeeShop.PlaceOrder(2 ​/* large cup */​);

That’s a tad better, but this is an occasion where commenting is used to compensate for poor code (Communicate in Code).

Java 5 and .NET (among others) have the concept of enumerated values. Let’s use it. We can define an enum named CoffeeCupSize in C# as the following:

 
public​ ​enum​ CoffeeCupSize
 
{
 
Small,
 
Medium,
 
Large
 
}

Then we can use it to order coffee:

 
coffeeShop.PlaceOrder(CoffeeCupSize.Large);

This code makes it obvious that we are placing an order for a large[25] cup of coffee.

As a developer, you should always be asking yourself whether there are ways to make your code easier to understand. Here’s another one:

Line 1 
public​ ​int​ compute(​int​ val)
{
int​ result = val << 1;
//... more code ...
return​ result;
}

What’s up with the shift operator in line 3? If you’re an experienced bit twiddler or familiar with logic design or assembly programming, then you may have figured that we just multiplied the value in val by 2.

But what about folks who may not have that background—will they figure that out? Perhaps you have some inexperienced team members who only recently made a career change into programming. These folks will scratch their heads until their hair falls out.[26] Although the code may be efficient, it lacks intent and expressiveness.

Shifting to multiply is an example of unnecessary and dangerous performance optimization. result = val*2 is clearer, works, and is probably even more efficient given a decent compiler (old habits die hard; see Know When to Unlearn). Instead of being too clever and opaque, follow the PIE principle: Program Intently and Expressively (see the sidebar here).

Violating the PIE principle can go beyond readability or understandability of code—it can affect its correctness. Here’s a C# method that tries to synchronize calls to the MakeCoffee method of a CoffeeMaker:

 
public​ ​void​ MakeCoffee()
 
{
 
lock​(​this​)
 
{
 
// ... operation
 
}
 
}

The author of this method wanted to define a critical section—at most one thread may execute the code in operation at any instant. To do that, the writer claimed a lock on the CoffeeMaker instance. A thread may execute this method only if it can acquire that lock. (In Java, you would use synchronized instead of lock, but the idea is the same.)

Although the code may look reasonable to any Java or .NET programmer, it has two subtle problems. First, the lock is too sweeping, and second, you are claiming a lock on a globally visible object. Let’s look at both these issues further.

Assume the coffeemaker can also dispense hot water, for those fans of a little Earl Gray in the morning. Suppose I want to synchronize the GetWater method, so I call lock(this) within it. This synchronizes any code that uses lock on the CoffeeMaker instance. That means you cannot make coffee and get hot water at the same time. Is that my intent, or did the lock become too sweeping? It’s not clear from reading the code, and you, the user of this code, are left wondering.

Also, the MakeCoffee method implementation claims a lock on the CoffeeMaker object, which is visible to the rest of the application. What if instead you lock the CoffeeMaker instance in one thread and then call the MakeCoffee method on that instance from another thread? At best it may lead to poor performance, and at worst it may lead to deadlock.

Let’s apply the PIE principle to this code, modifying it to make it more explicit. You want to keep more than one thread from executing the MakeCoffee method at the same time. So, why not create an object specifically for that purpose and lock it?

 
private​ ​object​ makeCoffeeLock = ​new​ ​object​();
 
 
public​ ​void​ MakeCoffee()
 
{
 
lock​(makeCoffeeLock)
 
{
 
// ... operation
 
}
 
}

This code addresses both the concerns we discussed—we rely on an explicit object to synchronize, and we express our intent more clearly.

When writing code, use language features to be expressive. Use method names that convey the intent; name method parameters to help readers understand their purpose. Exceptions convey what could go wrong and how to program defensively; use them and name them appropriately. Good coding discipline can help make the code more understandable while reducing the need for unnecessary comments and other documentation.

images/angel.png

Write code to be clear, not clever.

Express your intentions clearly to the reader of the code. Unreadable code isn’t clever.

What It Feels Like

You feel you—or anyone else on the team—can understand a piece of code you wrote a year ago and know exactly what it does in just one reading.

Keeping Your Balance

  • What’s obvious to you now may not be obvious to others, or to you in a year’s time. Consider your coding to be a kind of time capsule that will be opened in some unknowing future.

  • There is no later. If you can’t do it right now, you won’t be able to do it right later.

  • Writing with intent doesn’t mean creating more classes or types. It’s not an excuse for overabstraction.

  • Use coupling that matches the situation: for instance, loose coupling via a hash table is intended for a situation where the components really are loosely coupled in real life. Don’t use it for components that are tightly coupled, because that doesn’t express your intent clearly.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.131.72