on
Good method names
Everybody wants well-named methods — but what does that mean? We discuss the purpose of method names, guidelines for choosing them, and look at good and bad examples.
What are names for?
Method names tell code readers what the method does in its context. Context consists of both the elements in the method’s surroundings, and their level of abstraction.
Secondary, names make methods discoverable while writing more code.
It’s way more important to cater to readers than writers, because we write code once, but read it a lot more.
| While the examples in this post use Java, the guidelines apply to most object-oriented methods, and also other functions and operations. |
Guidelines
Follow naming conventions
Most teams and all programming languages have standard conventions for method names. Examples include "start with a verb", "don’t use underscores", or "start with lowercase character". Follow them.
If we don’t like them, first have a discussion about the criticisms, and adjust names after our team agreed.
It helps nobody to have method A.getName(), B.Get_Name() and C.name().
Exceptions
Some methods are only "shells" for other concepts, because the language requires it.
Prime example are unit tests:
In most languages, a unit test is a method (because that’s the only place we can write code).
It’s more important to describe what the unit test does than following naming conventions.
For example, Comparer_emptyLists() does not conform to Java conventions (doesn’t start with a verb, uses underscores).
But it clearly tells what this unit test is about: it tests the Comparer by feeding two empty lists.
The next method might be called Comparer_leftListEmpty() — signalling it’s also testing Comparer, but with different input data.
Don’t fear long names
Make the name as long as needed for good understanding. The length of a name is not an issue for any modern tool. The times when compilers only distinguished the first 32 characters of a name are long gone. We can also look at more than 25x80 characters on our screen by now.
Of course, there are limits: calculateInterestOfPositiveBalanceByCalendarYearDisregardingLeapYears() might describe what we’re doing, but does not help the reader.
At the time we’ve deciphered each camel-cased word, we’ve already forgotten what the name started with.
Struggling to find a descriptive short name might indicate our method does too many things. Consider splitting it up.
Nowadays nobody should type complete method names — that’s what we have code completion for.
If we have to type more than 5 characters to call a method, something is off:
To call calculateGeometricMean(), nobody should type more than cGM, calcG, GeoM or similar.
Either get a better editor / IDE, or declutter the codebase from redundant names.
Exceptions
Some abbreviated names are more established than their spelled out counterpart.
Most developer would first search for sqrt() before even thinking about squareRoot().
If a method is called very often in a section of code, longer names can clutter the section. One example is a classical interpreter implementation:
int eval(Node node) {
return switch (node) {
case Plus p -> eval(p.left) + eval(p.right);
case Minus m -> eval(m.left) - eval(m.right);
case Mul m -> eval(m.left) * eval(m.right);
case Div d -> eval(d.left) / eval(d.right);
case Num n -> Interger.parseInt(n.value);
};
}
This is easier to read than using the complete method name evaluateNode:
int evaluateNode(Node node) {
return switch (node) {
case Plus p -> evaluateNode(p.left) + evaluateNode(p.right);
case Minus m -> evaluateNode(m.left) - evaluateNode(m.right);
case Mul m -> evaluateNode(m.left) * evaluateNode(m.right);
case Div d -> evaluateNode(d.left) / evaluateNode(d.right);
case Num n -> Interger.parseInt(n.value);
};
}
Don’t repeat class or parameter names
A method appears in an outer scope (its class), and contains some scopes (parameter names and types). The class also appears in its own outer scope (depending on your programming language: module, namespace, package, etc.). Don’t repeat these names in the method name.
We usually know the context of the method, either because we’re inside the same class (this.detach()), or from the part before the dot: mySearchWidget.detach().
mySearchWidget.detachWidget() does not provide more information to the reader, but clutters the code and makes it harder to read.
The same applies to parameter types: myComparer.compare(leftNode, rightNode) tells us just as much as myComparer.compareNode(leftNode, rightNode).
Most probably, We’d write myComparer.compare(left, right), because we know in this context we always deal with Nodes.
If we change the name of the class or parameter, it’s easy to forget updating the method’s name.
A method Tree.createTreeLeaf() is understandable, but once we rename the class, Hierarchy.createTreeLeaf() is confusing.
Exceptions
Follow the naming conventions of our team or language.
Prime example are getters and setters: getName(String name) / setName(String name) implement Java’s naming convention for a name property on a class.
Some languages allow to add extension methods to foreign types.
For example, we can add a AppendNodeDescendants(Node source) method to the built-in List type and call myList.AppendNodeDescendants(myNode).
If we called the method AppendDescendants(), we might confuse users of List that don’t know about
Nodes and what descendants mean.
Let’s say we have some complex logic to add all relevant parts to a list.
We encapsulate this logic in a class PartsAdder with one public method.
add() is a better name than execute() or run(), even though it repeats the class name:
The reader better understands what the method does.
If our language doesn’t support overloading, we need to repeat the parameter type in the method name: addObject(Object newElement), addInt(int newElement).
The same applies if we’re using an untyped language.
Let’s assume we need to display the location of networked computers on a geographical map.
Both the network library, and the mapping library call their basic element Node.
Then method names like addNetworkNode(org.landiscover.network.Node node) and addMapNode(com.google.maps.Node node) are easier to understand than two add() methods.
We can also overuse overloading: If we had hundreds of eval() methods with different parameter types, the compiler can figure out which one gets called in each case — but for human readers it’s very hard to follow the call.
Move shared prefixes / suffixes to separate classes
If we had two sets of methods with common prefix or suffix, e.g.
class Calculator {
sumDouble(double a, double b);
multiplyDouble(double a, double b);
// vs.
sumInt(int a, int b);
multiplyInt(int a, int b);
}
we should try to move the methods to separate classes:
class DoubleCalculator {
sum(double a, double b);
multiply(double a, double b);
}
class IntCalculator {
sum(int a, int b);
multiply(int a, int b);
}
This especially applies to unit test classes:
If we had a CalculatorTest class containing
-
sumDouble_twoPositive() -
sumDouble_twoNegative() -
sumDouble_twoZero() -
multiplyDouble_twoPositive() -
multiplyDouble_twoNegative() -
multiplyDouble_twoZero() -
sumInt_twoPositive() -
sumInt_twoNegative() -
sumInt_twoZero() -
multiplyInt_twoPositive() -
multiplyInt_twoNegative() -
multiplyInt_twoZero()
we’d better split the class up in CalculatorDoubleTest and CalculatorIntTest, or even CalculatorDoubleSumTest, CalculatorDoubleMultiplyTest etc.
The methods would be called twoPositive(), twoNegative(), and twoZero().
Exceptions
If all the methods have to access a shared internal state (e.g. assume a "current value" field in the Calculator example above), we might need to compromise on good names.
Avoid "get" / "set" and "test" prefix
No other English words have more different meanings than set and get. Thus, we can use them for almost anything, but we should use them for almost nothing: They don’t tell the reader what we mean. The English language has thousands of other verbs we can use.
Use |
Instead of |
|---|---|
|
|
|
|
|
|
|
|
|
|
The same applies to "test" prefix in unit test classes:
Most methods in these classes are tests, and mostly they are marked by an annotation like @Test.
Moreover, often the containing class contains "Test" in its name.
Exceptions
For actual getters and setters, we want to follow the naming convention.
Tell a story in methods
A method should read like a story. As a method is mostly composed out of other method calls, each method name acts as the heading of the next part of the story. On the highest level, each called method name is like a heading of a chapter. Inside the chapter method, each called method name represents a section, and so on.
void executeTasks() {
initialize();
prepareCaches();
loopThroughTasks();
storeResults();
cleanup();
}
Bonus: Use "Test" as Prefix for test-only helper classes
This applies more to classes than methods, and is the only guideline geared specifically towards writing code.
We sometimes need supporting classes in our unit tests. Maybe they set up some shared environment, or implement a handler we need for testing. Such classes should have a "Test" prefix because:
-
"Test" suffix is sometimes used to discover actual unit test classes.
-
The prefix removes the class quickly from global searches for real code, both because "T" is late in the alphabet, and fuzzy name search is more sensitive to the beginning of names.
Example: Our code base contains several
ExceptionHandlerclasses, and we don’t exactly recall the name of the interesting one. Of course, we also write tests for exception handling, thus we also have such classes next to our unit tests. We might first search for*ExHandand get the following list:-
DatabaseExceptionHandler -
FatalExceptionHandler -
TestDummyExceptionHandler -
TestUiExceptionHandler -
UiExceptionHandler
We can quickly ignore anything starting with
Test, or refine our search toalExHandand hide all test handlers completely. -
Charts: Worst to best names
8th Comment instead of method
The worst possible method names are non-existing ones. The following code block is clearly structured in several sections — but by comments!
void executeTasks() {
// Initialize
initEnvironment();
cleanupTempFiles();
// Prepare Caches
precalculate();
fillCache();
// Loop through tasks
bool done = false;
while (!done) {
calculateNextTask();
done = checkMoreWork();
}
// Store results
collectResult();
writeResults();
// cleanup
closeFiles();
evictCaches();
}
This is bad for several reasons:
-
When the code evolves, it’s very easy to put a new line in the wrong section, or forget to update the comment. It’s way more likely to update a non-fitting method name than some arbitrary comment.
-
To get a quick overview of the implementation, we have to scan the code for the least expressive part — the comments. We also have to read through all lines, where in reality might be several hundreds of them.
-
With everything in one method, nobody stops us from creating some variables in the "Initialize" section, update them under some condition in the main loop, misuse them while storing results, and then use them erroneously in the "cleanup" section. In the better version below, we have to make a deliberate choice to pass such values in and out of each section, pushing us towards cleaner code.
void executeTasks() {
initialize();
prepareCaches();
loopThroughTasks();
storeResults();
cleanup();
}
void initialize() {
initEnvironment();
cleanupTempFiles();
}
void prepareCaches() {
precalculate();
fillCache();
}
void loopThroughTasks() {
bool done = false;
while (!done) {
calculateNextTask();
done = checkMoreWork();
}
}
void storeResults() {
collectResult();
writeResults();
}
void cleanup() {
closeFiles();
evictCaches();
}
This version reads like a story, and we (as a reader) can choose how many details we’d like to hear. As an analogy to "The Lord of the Rings", we can tell its story:
-
As brief as "Frodo gets the One ring, travels to Mordor, and destroys the jewel."
-
Be a bit more interested in the last part: "Frodo gets the One ring and travels to Mordor. He gets captured, but can escape. Disguised as orc, he crosses the Plateau of Gorgoroth. Finally, he scales Mount Doom. When Gollum wrestles the ring off Frodo’s finger, both the ring and Gollum fall and perish in fire."
-
By reading all 1157 pages of the trilogy.
The same applies to above’s version. We can quickly get a brief overview, and easily zoom in to any section of interest. This gets even easier with a proper editor or IDE that jumps to the implementation, and can navigate backwards on command (similar to a browser).
Exceptions
Some complex algorithms only work by juggling lots of different, always-changing data structures. It can be hard to cut such algorithms into independent parts. Don’t take this as a broad excuse: If our code is not straight out of a textbook or scientific paper, it probably doesn’t qualify.
If we’re absolutely sure, and have confirmed by measurements, that some part of code is performance-critical, we might need to sacrifice structure and naming for the sake of performance.
7th Decompiler names
Decompilers turn machine code back into some programming language. As machine code doesn’t contain any names, the decompiler must invent them. Sometimes they can guess, but often they use arbitrary, nonsensical gibberish.
void mA0we234__s() {
// ...
}
These names don’t tell us anything about the content, but at least we learn that some part of code belongs together, and (hopefully) could find a proper name for that part.
6th Same as method content
If we’re very uncreative how to name a method, we might just repeat the contents of the method.
int compareITo0(Integer i) {
i.CompareTo(0);
}
This is almost as bad as decompiler names, as it tells us how the method does its job — not what. It also doesn’t save us much time compared to just reading the code, and doesn’t give us a more abstract understanding.
Exceptions
While writing a piece of code, we might get disturbed by a block of code. Then we use our IDE to extract that block into a method — and now need a good name. However, we can easily start with any name, just to remove the disturbance. Later, possibly after several other changes, we can get back to the method. Now we understand our implementation better, and can give a fitting name to the method.
5th Narrate implementation
Just repeating the method content might seem ridiculous, but it’s very close to this next variant: narrate the implementation.
boolean checkIfIIsNotNullOrNegative(Integer i) {
i != null && i >= 0;
}
We have only a barely visible abstraction, and still have no idea about the context of the method. The name doesn’t save us any time compared to just reading the code. On the contrary: We might start thinking whether the name and the implementation actually say the same, and forget to update the name if we changed the content.
Exceptions
If our context tells everything else, seemingly narrative names can be ok.
class NumberParser {
private String input;
public NumberParser(String input) {
this.input = input;
}
public Number parse() {
checkNotNull();
determineNumberBase();
trim();
return convert();
}
private void checkNotNull() {
if (input != null)
return;
throw new IllegalValueException("input must not be null");
}
}
checkNotNull() narrates its content, but makes sense both in context of its class, and the story of the parse() method.
4th Inconsistent abstraction
As each method should tell a story, the names of the called methods should be on a similar level of abstraction.
int findIndexOf(Collection<String> haystack, String needle) {
var algorithm = selectSearchAlgorithm(haystack);
algorithm.prepare();
int index = algorithm.find(needle);
index = adjustIndexByMinusOneIfSearchAlgorithmIsOneBased(algorithm, index);
return index;
}
We have a clear story here: We select a good algorithm, prepare it, find our needle, and adjust the returned index to hide details of different algorithms.
The name adjustIndexByMinusOneIfSearchAlgorithmIsOneBased() tells us what the method does, but on a very different level of abstraction than the other called methods.
When we think about the problem to solve on this level, we get distracted by this level of detail.
3rd Leak implementation details
The method name should tell what it does, not how it achieves that goal.
class StringComparer {
boolean levenshteinDistanceSmallEnough(String left, String right) {
var levenshtein = new Levenshtein(left, right);
return levenshtein.distance() < MAX_DISTANCE;
}
}
Inside this StringComparer, we determine if the strings left and right are sufficiently similar.
Levenshtein distance
is probably a good algorithm for that task, but we don’t need to bother users of StringComparer with that detail.
2nd Too abstract
Good names do consider their context, but assuming a too broad or abstract context doesn’t help either.
class StringComparer {
boolean equalsOcrResult(String candidate, String ocrResult) {
var levenshtein = new Levenshtein(candidate, ocrResult);
return levenshtein.distance() < MAX_DISTANCE;
}
}
This is the same implementation as above, but the names assume we use it in the context of
optical character recognition.
There’s nothing specific about OCR on StringComparer, we could perfectly use it in other contexts.
Users might be thrown off by the misleading names.
1st Just right
Finally, an (almost) optimally named method.
Depending on our team’s conventions, we might need to prepend some verb to the name, e.g. compareSimilarity().
class StringComparer {
double similarity(String candidate, String baseline) {
var levenshtein = new Levenshtein(candidate, baseline);
int distance = levenshtein.distance();
return ((double) distance) - candidate.length();
}
}
The name clearly states what the method does, while hiding implementation details. It makes sense in its context without repeating parts of it, follows conventions, and doesn’t contain "get". The parameter names are easy to distinguish, and state their purpose. All called methods tell a story.
About Niko Stotz
I head the Model Driven Engineering team at F1RE, and support our colleagues and customers with language engineering and DSL expertise.
You can contact me at niko@f1re.io.