Mark Seemann helps programmers make source code easier to maintain. In this excerpt from his book, he provides guidance on naming methods in API design to avoid team confusion including x'ing out the name.
Just as comments can grow stale and misleading over time, so can method names. Hopefully you pay more attention to method names than comments, but it still happens that someone changes the implementation of a method but forgets to update the name.
Fortunately, with a statically typed language, you can use types to keep you honest. Design APIs so that they advertise their contracts with types. Consider the updated version of IReservationsRepository shown in the listing below. It has a third method named ReadReservation. It's a descriptive name, but is it sufficiently self-documenting?
One question I often find myself asking when I explore an unfamiliar API is: Should I check the return value for null? How do you communicate that in a durable and consistent way?
public interface IReservationsRepository
{
Task Create(Reservation reservation);
Task<IReadOnlyCollection<Reservation>> ReadReservations(DateTime dateTime);
Task<Reservation?> ReadReservation(Guid id);
}
Listing: IReservationsRepository with an additional ReadReservation method compared to Restaurant/ee3c786/Restaurant.RestApi/IReservationsRepository.cs
You could try to communicate with descriptive naming. For example, you might call the method GetReservationOrNull. This works, but is vulnerable to changes in behaviour. You might later decide to change the API design so that null is no longer a valid return value, but forget to change the name.
Notice, however, that with C#'s nullable reference types feature, that information is already included in the method's type signature. Its return type is Task<Reservation?></Reservation?>. Recall that the question mark indicates that the Reservation object could be null.
As an exercise in API design, try to x out the method names and see if you can still figure out what they do:
public interface IReservationsRepository
{
Task Xxx(Reservation reservation);
Task<IReadOnlyCollection<Reservation>> Xxx(DateTime dateTime);
Task<Reservation?> Xxx(Guid id);
}
What does it look like Task Xxx(Reservation reservation) does? It takes a Reservation object as input, but it doesn't return anything1. Since there's no return value, it must perform some sort of side effect. What might it be?
It could be that it saves the reservation. It might also conceivably transform it to an email and send it. It could log the information. This is where the defining object comes into play. When you know that the object that defines the method is called IReservationsRepository, the implied context is one of persistence. This enables you to eliminate logging and emailing as alternatives.
Still, it's not clear whether that method creates a new row in the database, or it updates an existing. It might even do both. It's also technically possible that it deletes a row, although a better candidate signature for a delete operation would be Task Xxx(Guid id).
What about Task<IReadOnlyCollection<Reservation>> Xxx(DateTime dateTime)? This method takes a date as input and returns a collection of reservations as output. It doesn't take much imagination to guess that this is a date-based query.
Finally, Task<Reservation?> Xxx(Guid id) takes an id as input, and may or may not return a single reservation. That's unambiguously an id-based lookup.
This technique works as long as objects afford only few interactions. The example has only three members, and they all have different types. When you combine method signatures with the name of the class or interface, you can often guess what a method does.
Notice, though, how it took more guesswork to reason about the anonymized Create method. Since there's effectively no return type, you have to reason about its intent based exclusively on the input type. With the queries, you have both input types and output types to hint at the method's intent.
X'ing out method names can be a useful exercise, because it helps you empathise with future readers of your code. You may think that the method name you just coined is descriptive and helpful, but it may not be to someone with a different context.
Names are still helpful, but you don't have to repeat what the types already state. This gives you room to tell the reader something he or she can't divine from the types.
Notice the importance of keeping the tools sharp, so to speak. This is another reason to favour specialised APIs over Swiss Army knifes. When an object only exposes three or four methods, each method tends to have a type distinct from the other methods in that context. When you have dozens of methods on the same object, this is less likely to work well.
The method types are most likely to be helpful when the types alone disambiguate them from each other. If all methods return string or int, their types are less likely to be helpful. That's another reason to eschew stringly typed APIs.
1 Strictly speaking, it returns a Task, but that object contains no additional data. Regard Task as the asynchronous equivalent of void.