Thursday, October 27, 2011

Guideline for Returning Value From a Method

There are two different scenarios when you return an object from  a method. If everything goes well, you get what you expect. If not, in my opinion, there are 3 different approaches:
  1. Return null
  2. Return concrete object with default properties
  3. Throw a meaningful exception 
For example. You have product id to be passed to a method (GetById) to retrieve concrete Product object. If Product is found on your database-resource, GetById() returns a Product object which is not null. This is best case scenario.

What if Product is not found or parameters are wrong, what should GetById() method return? null or concrete Product object whose properties have default value or a meaningful exception. Let's see all 3 approaches' advantages and disadvantages.


Returning null
When I asked my colleagues and my friends, most of all they prefer this approach, returning null. But I have concerns about it.

Disadvantages:
  • Everywhere on your code you have to check whether returning object is null. 
  • It's always possible to forget to check if object is null. It causes runtime error.
  • Code file might be full of if statements. It makes code unreadable and dirty.

Returning concrete object which is not null but whose properties have default value

Disadvantages:
  • You're always doubt about to know if record is really found on database-resource.
  • You may miss a serious problem.
  • You might need to check unique id of the concrete object (for instance product id) to make sure returning value is valid. However it's useless. Every object doesn't need to have unique id. Beside that, it's as same solution as checking null object.

Throw a meaningful exception
Throwing a meaningful exception is my favorite solution. By applying this practice, you eliminate all disadvantages above. But it comes with its own disadvantages which are manageble.
 
Advantages :
  • You can exactly know what the problem is.
  • You don't have to return an magic number to identify the result. Such as, 1 is ok, 2 is parameters are missing, 3 is object not found.
  • You don't have to check if object is null. Throw the exception to up-level. Your try-catch block on up-level decides how to react.

Disadvantages :
  • You need to manage exceptions properly. Some exceptions doesn't need to be logged or shown to end user.
  • You might need to have custom exception classes to identify exact problem. For instance, instead of throwing object is null exception,  you might need to use ProductNotFound exception class.
Sample Code 
You can find sample code guideline about returning meaningful value from a method. Please focus on code between row number 29 - 44.

In Main method I created an int array to test all possibilities. In catch block you can decide what you want. You might need to continue the process or stop to run the code and exit the application or show error message to end user.

Id = 1; the method returns a Product object which is not null. It's the best case.
Id = 6; the method throws ProductNotFound exception.
Id = -5 and Id = 0; the method throws ArgumentOutOfRangeException.

As you see the place where I invoke GetById() method, I don't check whether object is null. All validations and strategies are handled in GetById() method which is in business layer.

   1:  public class ProductRepository
   2:  {
   3:      private List<Product> Products;
   4:      public ProductRepository()
   5:      {
   6:          Products = new List<Product> {
   7:              new Product { Id = 1, Name = "harddisk" },
   8:              new Product { Id = 2, Name = "mouse" },
   9:              new Product { Id = 3, Name = "keyboard" },
  10:              new Product { Id = 4, Name= "speaker"}
  11:          };
  12:      }
  13:   
  14:      public Product GetById(int id)
  15:      {
  16:          return Products.SingleOrDefault(x => x.Id == id);
  17:      }
  18:  }
  19:   
  20:  public class ProductManager
  21:  {
  22:      private ProductRepository repository;
  23:   
  24:      public ProductManager() 
  25:      {
  26:          repository = new ProductRepository();    
  27:      }
  28:   
  29:      public Product GetById(int id)
  30:      {
  31:          // validate input
  32:          if (id < 1)
  33:              throw new ArgumentOutOfRangeException("id=" + id);
  34:   
  35:          // run exact code
  36:          Product result = repository.GetById(id);
  37:   
  38:          // validate result
  39:          if (result == null)
  40:              throw new ProductNotFound("id="+id+" not found");
  41:   
  42:          return result;
  43:      }
  44:  }
  45:   
  46:  public class ProductNotFound : Exception 
  47:  {
  48:      private string customMessage;
  49:      public ProductNotFound(string message) 
  50:      {
  51:          customMessage = message;
  52:      }
  53:   
  54:      public ProductNotFound() { }
  55:   
  56:      public override string Message
  57:      {
  58:          get
  59:          {
  60:              return base.Message + " " + customMessage;
  61:          }
  62:      }
  63:  }
  64:   
  65:  public class Product
  66:  {
  67:      public int Id { get; set; }
  68:      public string Name { get; set; }
  69:  }
  70:   
  71:  class Program
  72:  {
  73:      static void Main(string[] args)
  74:      {
  75:          int[] ids = new int[] { 1, 6, -5, 0 };
  76:          int errorCount = 0;
  77:   
  78:          ProductManager manager = new ProductManager();
  79:          foreach (int item in ids)
  80:          {    
  81:              try
  82:              {
  83:                  Product product = manager.GetById(item);
  84:              }
  85:              catch (Exception ex)
  86:              {
  87:                  errorCount++;
  88:                  Console.WriteLine(ex.Message);
  89:              }
  90:          }
  91:   
  92:          Console.WriteLine("Error Count : " + errorCount);
  93:          Console.Read();
  94:      }
  95:  }

0 comments: