solution

The Leaky Exception Solution

solution

 

Today’s article in assumes knowledge from yesterday’s The Leaky Exception Problem. If you haven’t read it yet, I highly recommend you do so first.

In The Leaky Exception Problem, we examined the situation where our code catches exceptions that are leaking implementation details across an abstraction boundary. Each additional implementation of the abstracting interface required another specific exception handler.

Today we’ll investigate a solution to the problem. 

To recap, the exception handling for a call to an ICustomerRepository’s AddCustomer() method looked as shown below. The problem we have is that the catch blocks leak knowledge of SQL Server and Mongo DB adapter implementations:

  try
  {
     CustomerRepository.AddCustomer(customer);
  }
  catch (SqlException sqlEx)
  {
     // code to identify whether it's a transient, recoverable error,
     // and if it is we can retry the database operation. Otherwise 
     // the exception is not recoverable and we rethrow it. 
  }
  catch (MongoDbException mongoEx)
  {
     // code to identify whether it's a transient, recoverable error,
     // and if it is we can retry the database operation. Otherwise 
     // the exception is not recoverable and we rethrow it. 
  }

What would we like the code to be? I think this would be much better:

  try
  {
     CustomerRepository.AddCustomer(customer);
  }
  catch (DataException dataEx)
  {
     // already know it's recoverable error => retry the operation. 
  }

Now when we have a transient failure in AddCustomer(), we catch a general DataException. Because it’s a DataException, we know that we can recover from it and retry. How do we know this? Well, we don’t – we’re designing our ideal solution here. So, by our definition, DataException is recoverable. That decision, though, is made somewhere else. The only place for that choice to be performed is in each of the adapter implementations, SqlServerCustomerRepository and MongoDbCustomerRepository. 

What does this mean? The responsibility of catching specific exceptions is delegated to SqlServerCustomerRepository and MongoDbCustomerRepository. It’s these adapters that will

  1. catch the specific exception (e.g. SqlException), and
  2. determine whether it is recoverable, and
  3. if it is, recast and throw it as a general DataException 

Note: A nice touch in .NET is to retain the original exception as the InnerException for debugging. Thank you for pointing this out, Nam!  

Now when we create a new persistence adapter, say, FileCustomerRepository, that writes and reads customer records to/from a file. Do we need to make changes to our code calling AddCustomer()? No, we shouldn’t need to. Not as long as we catch and rethrow any recoverable file exceptions as DataExceptions in FileCustomerRepository. 

What is really cool is that we have managed to become compliant with the Open-Closed Principle: In adding FileCustomerRepository, we only wrote new code, and we didn’t need to modify existing code. 

That is the essence of pluggable, highly maintainable software.

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply