Design Tip - Write Honest Methods

Problem

As developers, we learn that giving meaningful names to methods produces clean, readable and maintainable code. However, that’s not all we should be concerned about when it comes to method signatures. There are two other aspects of a method signature that must be given consideration when writing code: a) parameters b) return value.

Let’s look at the signature of a method we’ve defined in our repository, what does this tell you?

  1. public Movie FindById(int id) { ... }  
It promises that if you pass int, you will receive a custom Movie type. A fellow developer will use this method like.
  1. var movie = FindById(5);              
  2. // use movie  
This application goes live and suddenly users receive a null reference exception. Now what? The fellow developer decides to have a look ‘under the hood’.
  1. public Movie FindById(int id)  
  2. {  
  3.     if id not found  
  4.         return null;  
  5.   
  6.     return new Movie(); // id found  
  7. }  
Ah, so the method returns a null if record isn’t found in the database.

Let’s look at another signature of a method we’ve defined in our service, what does this tell you?

  1. public bool UploadDocument(string fileName, string fileExtension) { ... }  
It promises that if you pass two string parameters, you will receive a bool type. It’s not clear what the bool represents, e.g., does false mean exception or validation issue? We’ll need to look at the implementation once again.
  1. public bool UploadDocument(string fileName, string fileExtension)  
  2. {  
  3.     if (string.IsNullOrEmpty(fileName))  
  4.         throw new ArgumentException("file name empty");  
  5.     if (string.IsNullOrEmpty(fileExtension))  
  6.         throw new ArgumentException("file extension empty");  
  7.   
  8.     if (fileExtension == "pdf" ||   
  9.          fileExtension == "doc" ||   
  10.          fileExtension == "docx")  
  11.     {  
  12.         // upload  
  13.   
  14.         return true;  
  15.     }  
  16.     else  
  17.     {  
  18.         return false;  
  19.     }  
  20. }  
Now we notice that this method could throw exceptions too, without knowing the implementation this will break the application at runtime. Also, we discover that the boolrepresents a validation issue.

These examples are simplistic but highlighted a few key points about these method signatures,

  • Method signatures did not convey their true input and output – they were dishonest.
  • In order to understand the input/output, we had to look into the implementation of the method – they were not well encapsulated.
  • Input and output were ambiguous, without implementation details their meaning was unclear – they were not semantically readable.
  • It is only at runtime that an invalid value is caught – the design is not defensive, allows a programmer to write code that could potentially fail.

How can we improve on this? I’ll provide one solution that I prefer – honest methods using domain abstractions as parameters and return value.

Solution

Making your method signatures as specific as possible make them easier to consume and less error-prone. One technique is to think in terms of concepts present in the business domain for which we’re writing the application and then using custom types to pass data in and out of methods. Using custom instead of primitive types provide richer semantic meaning to method signature and thus help with the readability of the code.

Back to our examples, let’s say that the development team creates a custom type to be used for the application which will contain either the return value or the error message. They name it Result. Let’s look at modified method signature from our earlier example, what does this tell you,

  1. public Result<Movie, Error> FindById(int id) { ... }  
It promises that if you pass int, you will receive a custom Result type, containing either the Movieor Error type. The caller should now ‘expect’ that something can go wrong when calling the method, it’s clear from the signature. An honest function always does what its signature says, and given an input of the expected type, it yields an output of the expected type—no Exceptions, no nulls.

FAQ

What if the developer using the method doesn’t know what Result is? This would be part of team coding standards/guidelines. Then it is no different than using built-in types like int, float, enum etc. Developers know what to store in them. The custom type here has a clear semantic meaning, it would hold return value or a failure message.

Our next example is little more interesting, let’s look at modified method signature, what does this tell you?

  1. public UploadImageResult UploadDocument(UploadFile file) { ... }  
It promises that if you pass custom type UploadFile, you will receive a custom type UploadImageResult. Perhaps this is not so clear to our fellow developer, but at least he can’t ‘accidently’ misuse it. However, this doesn’t necessarily require him to know the implementation of this method. He only needs to know how to create these custom data types.

To make it even more interesting, let’s say he/she doesn’t even have access to source code for this method. He decides to use intelliSense.

  • Intellisence for service.
  • Service needs UploadFile, intelliSense for it.
     
  • Service returns UploadImageResult, intelliSense for it.

The public API for the method is discoverable and makes it difficult for the developer using it to get it wrong. We still have the guard clauses and validation, however, these are encapsulated within the UploadFile type.

  1. public sealed class UploadFile  
  2. {  
  3.     public UploadFile(string fileName, string fileExtension)  
  4.     {  
  5.         if (string.IsNullOrEmpty(fileName))  
  6.             throw new ArgumentException("file name empty");  
  7.         if (string.IsNullOrEmpty(fileExtension))  
  8.             throw new ArgumentException("file extension empty");  
  9.         if (!IsValidFormat(fileExtension))  
  10.             throw new ArgumentException("incorrect file format");  
  11.   
  12.         FileName = fileName;  
  13.         FileExtension = fileExtension;  
  14.     }  
  15.   
  16.     public string FileName { get; }  
  17.     public string FileExtension { get; }  
  18.     private bool IsValidFormat(string fileExtension)  
  19.         => (new[] { "pdf""doc""docx" }).Contains(fileExtension)  
  20. }  
FAQ

UploadFile still requires string parameters, are we not just moving the problem of primitive types and parameter guard clauses to a different place? Yes, we are. We are moving it up the call stack. We ‘do’ need these checks, but now they are - a) part of object creation b) encapsulated in a type c) help with ‘failing fast’.

FAQ

Why avoid throwing exceptions in a method and instead advertise failure using return type? By throwing an exception, you’re breaking the application and making an assumption that caller will catch it. You’re also coupling the caller with implementation detail of a method, breaking its encapsulation. The caller should depend only on input and output of a method, exceptions are neither of them.

When designing the public interface of our classes and methods, we should try to avoid its misuse. Creating methods with honest signatures is a good technique to hide implementation details (encapsulation), reduce bugs and improve code readability.

Note
This is not the only way to design your code, even if you don’t agree with my solution, hopefully, it will add a technique to your repertoire.

Source Code