Peter Streef

Naming Things

Battle unicorn

Naming things might be hard, but consistently naming similar things should not be! Years ago I started using a list of method name prefixes with rules on when to use them. Since then I’ve introduced it successfully within 2 teams to make collaboration easier. These are most applicable for OO, but not exclusively.

🙋 Why is readability so important

It’s been known for a while that a lot of a developers time goes to understanding the code worked on. According to this comprehensive study 60-80% of time is spend understanding code. You can find a more thorough explanation here.

This leaves a lot of room for optimization!

🔑 Consistency is key

When every developer on a team uses the same patterns to name their methods, variables and classes it becomes much easier for the next person to get up to speed. Not only when naming their own things, but more importantly when trying to understand what was written before.

🧅 Encapsulation at the cost of understanding

We are taught to encapsulate our data in our models by exposing behaviour, this often ends up with our classes having lots of getters even though under the hood a lot more is going on than simply “getting” some data. An argument can be (and has been) made that it should not matter how a class is getting this data, but when you have to debug or change such behaviour it often does help to have a little more hints as to what is going on under the hood. Especially when using a classical layered architecture and multiple services call methods with the same name in an ever deeper layer, it becomes really hard to grasp why each layer exists.

📛 Method name patterns

This paragraph lists method prefixes and rules around when they should be used. These prefixes should be followed by a logical explanation of what the method does. Note that it is almost always better to have a longer method name when it can avoid even 1 developer misunderstanding or having to figure it out from the implementation.

Get

Basic getter. These methods are meant to get data out of objects, this data should in most cases directly relate to a field of the object, however, in some situations it might be required to calculate/determine some value based on other data of the object and to expose it for serialization purposes. Also lazy initialization is allowed. But avoid more complex behaviour or things that can cause exceptions.

Static Args Returns Throws
0 value
public Something getSomething() {
    return something;
}
// lazy initialization
public List<Something> getSomethings() {
   if (somethings == null) {
       somethings = new ArrayList();
   }
   return somethings
}

Set

Basic setter. These methods are meant to set fields on an object. Avoid any complex behaviour or possible exceptions.

Static Args Returns Throws
1 void
public void setSomething(Something something) {
    this.something = something;
}

With

Called a wither. It can be used to set a field on an object, or create a new immutable object copy with the specific field set to the given value. Returning the object is meant for chaining method calls.

Static Args Returns Throws
1 this
// immutable
public Parent withSomething(Something something) {
   return this.toBuilder().something(something).build();
}

// mutable
public Parent withSomething(Something something) {
  this.something = something;
  return this;
}

Of

Static constructor defined on the return type class. It’s meant to transform (or build up from) many things into a new instance of an object.

Static Args Returns Throws
✔️ 1-n instance input validation
public class Parent {
  public static Parent of(Something a, Other b) {
    return Parent.builder().a(a).b(b).build();
  }
}

From

Special static constructor defined on the return type class. It’s meant to transform one one POJO into another.

Static Args Returns Throws
✔️ 1-n instance input validation
public class Parent {
  public static Parent fromSomething(Something something {
    return Parent.builder().something(something).build();
  }
  
  public static Parent of(Something a, Other b) {
    return Parent.builder().a(a).b(b).build();
  }
}

To

Instance transformation method to transform a POJO into another POJO.

Static Args Returns Throws
0 instance input validation
public class Parent {
  String name;
  public Something toSomething() {
    return Something.builder().parentName(name).build();
  }
}

Retrieve

Method for retrieving something from another place. This can be another internal service, an external service or a database.

Static Args Returns Throws
0-n object or (Possibly empty) collection input validation or object not found. Don’t throw on empty collection
// collection
public Collection<Thing> retrieveThingsForCustomer(Customer customer) {
    returns db.findAllByCustomerId(customer.getId());
}

// single
public Thing retrieveThingById(String id) {
   retruns db.findOne(id)
            .orElseThrow(() -> new NotFoundException("Thing not found by id " + id));
}

Find

Method for optionally retrieving something from another place. This can be another internal service, an external service or a database.

Static Args Returns Throws
0-n Optional input validation or technical issues. Don’t throw on object not found
public Optional<Thing> findThingById(String id) {
   retruns db.findOne(id);
}

Determine

Service, model or util method executing some business logic to determine an outcome. Useful on model classes in stead of getters for non existing fields, but also for more complex logic.

Static Args Returns Throws
🤷 0-n value or Object input validation or technical issues
public Instant determineExpiresAt() {
    return Instant.now(clock).plusSeconds(EXPIRE_IN_SECONDS);
}

Calculate

Service, model or util method executing some business logic to calculate an outcome. This is a more specific form of Determine that it should not do any I/O and only use CPU time.

Static Args Returns Throws
🤷 0-n value or Object input validation
public class Rectangle {
  float width;
  float height;
 
  public float calculateArea() {
    return width * height;
  }
}

Generate

Generate a new object from internal data, never by external source. Usually without arguments, but there can be exceptions to steer the generation process. Acts like a constructor, but is usually defined in a service or helper.

Static Args Returns Throws
🤷 0(-n) Object input validation
public static Fixture generateFixture() {
   retrurn Fixture.builder().value(1).build();
}

Create

Create something new from (partial) data in internal or external source like via the API of another service or in a database. Acts like a constructor or builder, but is defined in a service or helper.

Static Args Returns Throws
🤷 1-n value or Object input validation
public static Fixture createFixture(String variable) {
   retrurn Fixture.builder().value(1).variable(variable).build();
}

Check

Validate business rule and return true/false. Usually on an entity or util class

Static Args Returns Throws
🤷 0-n boolean
public class Order {
  Collection<Items> items;
  boolean checkAllItemsAvailable() {
    return items.stream().allMatch(Item::isAvailable);
  }
}

Validate

Validate business rule and throws if invalid. Usually on an entity or util class. Combines well with a check method.

Static Args Returns Throws
🤷 0-n void if invalid
public class Order {
  Collection<Items> items;

  boolean checkAllItemsAvailable() {
    return items.stream().allMatch(Item::isAvailable);
  }

  boolean validateAllItemsAvailable() {
    if(!checkAllItemsAvailable()) {
      throw new ItemAvailabilityException();
    }
  }
}

Keep

Filters collection and keeps only items that meet certain condition.

Static Args Returns Throws
🤷 1-n Collection
private static Collection<Item> keepItemsWithColor(Collection<Item> items, Color color) {
  return items.stream()
              .filter(item -> color.equals(item.getColor()))
              .toList();
}

☘️ Field name patterns

Field names are a lot harder to standardize on, but there are a few categories where it is easy to achieve.

Time

A timestamp should be identified by the suffix _at or At:

Instant createdAt;
Instant updatedAt;

Prefer updated over modified. Prefer created over started.

An exception is when the timestamp does not have a predefined meaning. Then we will fall back to timestamp.

Changed by

A change author should be identified by a field with a _by or By suffix:

String createdBy;
Author updatedBy;

Depending on your usecase this could either be a simple String or an object containing more data.

State

You might want to track the state of a process. I like to use a state history so it is easy to track state transitions over time:

{
  "stateHistory" : [
    {
      "status": "CREATED",
      "timestamp" : "2022-08-24T10:11:12.000Z"
    },
    {
      "status": "QUEUED",
      "timestamp" : "2022-08-24T10:11:12.100Z"
    },
    {
      "status": "IN_PROGRESS",
      "timestamp" : "2022-08-24T10:11:12.200Z"
    },
    {
      "status": "FAILED",
      "timestamp" : "2022-08-24T10:11:12.250Z"
      "message" : "500 internal server error"
    },
    {
      "status": "RETRIED",
      "timestamp" : "2022-08-24T10:11:12.300Z"
      "message" : "Retrying 1/3 times"
    },
    {
      "status": "IN_PROGRESS",
      "timestamp" : "2022-08-24T10:11:12.400Z"
    },
    {
      "status": "SUCCESSFUL",
      "timestamp" : "2022-08-24T10:11:12.500Z"
    }
  ]
}

💡 Conclusion

If you can convince your team to settle on any convention for method names you are already winning. Feel free to use these, or come up with your own!