This is an excerpt from my new book, “Clean Code in JavaScript“.


Breaking down the key elements of a good name is difficult. It seems to be more of an art than a science. The boundary between quite a good name and a very good name is fuzzy and liable to subjective opinions.

Consider a function that is responsible for applying multiple CSS styles to a button.

Imagine a scenario in which this is a standalone function. Which of the following names would you consider to be most suitable?

  • styleButton
  • setStyleOfButton
  • setButtonCSS
  • stylizeButton
  • setButtonStyles
  • applyButtonCSS

You’ve likely picked your favorite. And there is, amongst the readers of this book, bound to be disagreements. Many of these disagreements will be founded in our own biases. And many of our biases will have been conditioned by factors like what language we speak, what programming languages we’ve been previously exposed to, and what types of programs we spend our time creating. There are many variances that exist between all of us, and yet somehow, we have to come up with a non-fuzzy concept for what a “good” or “clean” name is. At the very least, we can say that a good name might have the following characteristics:

  • Purpose: What something is for and how it behaves.
  • Concept: Its core idea and how to think about it.
  • Contract: Expectations about how it works.

This doesn’t completely cover the complexity of naming, but with these three characteristics, we have a starting point. In the remainder of this section, we will learn how each of these characteristics is vital to the process of naming things.

Purpose

A good name indicates purpose. Purpose is what something does, or what something is. In the case of a function, its purpose is its behavior. This is why functions are typically named in the verbal form, like getUser or createAccount, whereas things that store values are usually nouns, like account or button.

A name that encapsulates a clear purpose will never need further explanation. It should be self-evident. If a name requires a comment to explain its purpose then that is usually an indicator that it has not done its job as a name.

The purpose of something is highly contextual and so will, therefore, be informed by the surrounding code and the area of the codebase in which that name resides. This is why it’s often okay to use a generic name as long as it is surrounded by context that helps to inform its purpose. For example, compare these three method signatures within the class TenancyAgreement:

class TenancyAgreement {
 
    // Option #1:
    saveSignedDocument(
        id,
        timestamp
    ) {}
 
    // Option #2:
    saveSignedDocument(
        documentId,
        documentTimestamp
    ) {}
 
    // Option #3:
    saveSignedDocument(
        tenancyAgreementSignedDocumentID,
        tenancyAgreementSignedDocumentTimestamp
    ) {}
}

There are subjectivities to this, of course, but most people would agree that, when we have a surrounding context that communicates its purpose well, we shouldn’t need to granularize the naming of every variable within that context. With this in mind, we can say that Option #1 in the preceding code is too limited and may invite ambiguity, and option #3 is needlessly verbose, as parts of its argument names are already provided by its context. Option #2, however, with documentId and documentTimestamp, is “just right“: it sufficiently communicates the purpose of the arguments. And this is all we need.

Purpose is absolutely central to any name. Without a description or an indication of purpose, a name is merely decoration, and can often mean that users of our code are left rummaging around between documentation and other pieces of code just to figure something out. So we must remember to always consider whether our names communicate purpose well.

Concept

A good name indicates concept. A name’s concept refers to the idea behind it, the intent in its creation, and how we should think about it. For example, a function named relocateDeviceAccurately tells us not only what it will do (its purpose) but it informs us about the concept surrounding its behavior. From this name we learn that devices are things that can be located, and that the locating of such devices can be done at different levels of accuracy. A relatively simple name can arouse a rich concept within the minds of those who read it. This is part of the vital power of naming things: names are avenues to understanding.

A name’s concept, like its purpose, is strongly tied to the context in which it exists. Context is the shared space that our names exist within. The other names that surround the name we’re interested in are absolutely instrumental in helping us understand its concept. Imagine the following names seen together:

  • rejectedDeal
  • acceptedDeal
  • pendingDeal
  • stalledDeal

By these names, we immediately understand that a deal is something that can have at least four different states. It is implied that these states are mutually exclusive, and cannot apply to a deal at the same time, although that is unclear at this time. We are likely to assume that there are specific conditions related to whether a deal is pending or stalled, although we’re not sure what those conditions are. So, even though there is an ambiguity here, we are already starting to build up a rich understanding of the problem domain. That’s just by looking at names–without even reading the implementation.

We have spoken about context as a kind of shared space for names. In programming vernacular, we usually say that things named together in one area occupy a single namespace. A namespace can be thought of as a place where things share a conceptual area with each-other. Some languages have formalized the concept of a namespace into its own language construct (often called a package, or simply: a namespace). Even without such formal language constructs, JavaScript still makes it possible to construct namespaces via hierarchical constructs like objects. For example:

const app = {};
app.transactions = {};
app.transactions.dealMaking = {};
app.transactions.dealMaking.states = [
     'REJECTED_DEAL',
     'ACCEPTED_DEAL',
     'PENDING_DEAL',
     'STALLED_DEAL'
];

Most programmers tend to think of namespaces as a very formal construct, but it’s not so. Often without knowing it, we are composing implied namespaces when we write functions with functions within them. Instead of being delineated by a level of an object hierarchy, the “namespaces” in this case are delineated by the scopes of our functions. For example:

function makeFilteredRequest(endpoint, filterFn) {
    return fetch(`/${endpoint}/`)
        .then(response => response.json())
        .then(data => data.filter(filterFn);
}

Here we are making a request to an endpoint, via fetch, and before we return we are first gathering the required data via tapping into the Promise returned by fetch. To do this we use two then(…) handlers.

N.B. A Promise is a natively provided class that provides a useful abstraction for handling asynchronous actions. You can usually identify a promise by its then method, like that used within the preceding code. It’s common practice to either use promises or callbacks when tapping into asynchronous actions. You can read more about this in Chapter 10, “Control Flow” under the heading “Asynchronous Control Flow“.

Our first then(…) handler names its argument response, and the second one names its argument data. Outside the context of makeFilteredRequest, these terms would be very ambiguous. Yet because we are within the implied ‘namespace’ of a function related to making a filtered request, the terms response and data are sufficient to communicate their concepts.

The concepts communicated by our names, much like their purposes, are heavily intertwined with the contexts in which they are specified, so it’s important to consider not only the name itself but everything that surrounds it: the complex mesh of logic and behavior in which it resides. All code deals with some level of complexity, and conceptual understanding of that complexity is crucial in being able to harness it: so, when naming something, it helps to ask yourself: How do I want them to understand this complexity? This is relevant whether you’re crafting a simple interface to be consumed by other programmers, writing a deeply embedded hardware driver, or creating a GUI for non-programmers to consume.

Contract

A good name indicates a contract with other parts of the surrounding abstraction. A variable, by its name, may indicate how it will be used, or what type of value it contains, and what general expectations we should have about its behavior. It’s not usually thought about, but when we name something we are, in fact, setting up a series of implicit expectations or “contracts” that will define how people understand and use that thing. Here are some examples of the hidden contracts that exist in JavaScript:

  • A variable prefixed with ‘is‘, for example, isUser, is expected to be a boolean type (either true or false).
  • A variable in all-caps is expected to be a constant (only set once and immutable), e.g. DEFAULT_USER_EXPIRY
  • Variables named plurally (e.g. elements) are expected to contain one or more items in a set-like object (e.g. an Array), whereas singularly named variables (e.g. element) are only expected to contain one item (not in a set).
  • Functions with names beginning with get, find or select (etc.) are usually expected to return something to you. Functions beginning with process, build or run are more ambiguous and may not do so.
  • Property or method names beginning with an underscore, such as _processConfig, are usually intended to be internal to an implementation or pseudo-private. They are not intended to be called publicly.

Whether we like it or not, all names carry with them the baggage of unavoidable expectations regarding their values and behaviors. It’s important to be aware of these conventions so that we do not accidentally break the contracts that other programmers rely on. Every convention will have an exception where it doesn’t apply, of course, but nonetheless, we should try to abide by them where possible.

Unfortunately, there isn’t a canonical list where we can find all of these contracts defined. They are usually quite subjective and will depend a lot on the codebase. Nonetheless, where we do encounter such conventions, we should seek to follow them. As covered in the second chapter, ensuring familiarity is a great way to increase the maintainability of our code. And there is no better way to ensure familiarity than adopt conventions that other programmers have come to adopt.

Many of these implied contracts are related to types, and JavaScript, as you may be aware, is dynamically typed. This means the types of values will be determined at runtime, and the type contained by any variable may be liable to change:

var something;
something = 1; // a number
something = true; // a boolean
something = []; // an array
something = {}; // an object

The fact that a variable can refer to many different types means that the contracts and conventions implied by the names we adopt are even more important. There is no static- type checker to help us. We are left alone at the chaotic whim of ourselves and other programmers.

N.B. Later in the chapter, we discuss Hungarian Notation, a type of naming that is useful in dynamically-typed languages. Also, it’s useful to know that there are various static-type-checking and type-annotating tools available for JavaScript, if you find dealing with its dynamism painful. These are covered in Chapter 15 (Tools for Cleaner Code).

Contracts are not only important because of JavaScript’s dynamically-typed nature. They are fundamentally useful in giving us confidence in how certain values behave and what we can expect from them throughout the runtime of our program. Imagine if there was an API with a method called getCurrentValue() that didn’t always return the current value. That would break its implied contract. Seeing names through the lens of contracts is quite a mind-warper. Soon you will begin to see contracts everywhere — contracts between variables, between interfaces, and at the integration level between entire architectures and systems.

Now that we’ve discussed the three characteristics of a good name (Purpose, Concept, Contract) we can begin to explore some anti-patterns, i.e. ways of naming things that we should try to avoid.


This was an excerpt from my new book, “Clean Code in JavaScript“.

Thanks for reading! Please share your thoughts with me on Twitter. Have a great day!