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

Unit tests (and other "shell" methods)

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

Established names

Some abbreviated names are more established than their spelled out counterpart. Most developer would first search for sqrt() before even thinking about squareRoot().

Repetition

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:

Java example with short repeated method name
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:

Java example with complete, long repeated method name
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

Naming conventions

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.

Extension methods

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.

Encapsulation

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.

Overloading or untyped language

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.

Disambiguation in same context

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

Required by program structure

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.

Table 1. Examples to avoid "get" / "set"

Use

Instead of

rename()

setName()

discount()

setNewDiscount()

calculate()

getValue()

evaluate()

getResult()

collect()

getAll()

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

Naming conventions

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 ExceptionHandler classes, 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 *ExHand and get the following list:

    • DatabaseExceptionHandler

    • FatalExceptionHandler

    • TestDummyExceptionHandler

    • TestUiExceptionHandler

    • UiExceptionHandler

    We can quickly ignore anything starting with Test, or refine our search to alExHand and 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!

Bad example of missing method (names)
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.

Good refactoring extracting each commented section as separate method
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

Complex algorithms

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.

Performance optimization

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

Temporary names during development

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

Rich context

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.