If you write a lot of code that takes dependencies in a method or constructor, you'll find that you write a lot of null checking like this:
public DefaultGame(ILoggingService loggingService, IWorldService worldService)
{
if (loggingService == null)
{
throw new ArgumentNullException("loggingService", "Logging Service must not be null!");
}
else if (worldService == null)
{
throw new ArgumentNullException("worldService", "World Service must not be null!");
}
this.loggingService = loggingService;
this.worldService = worldService;
}
Depending on the number of dependencies, this tends to bloat methods with more safety checks than actual code. I wanted to resolve this, and did so with a factory.
The factory needed to satisfy a few things.
- Instance exceptions
- Support stashing custom data in to the exceptions
- Only throw if a given condition was met
- Allow a callback if the conditions are not met (meaning no exception will be thrown)
Turns out this was pretty easy to implement. The end product, in its simplest form looks like this:
public DefaultGame(ILoggingService loggingService, IWorldService worldService)
{
ExceptionFactory
.ThrowExceptionIf< ArgumentNullException>(loggingService == null);
ExceptionFactory
.ThrowExceptionIf< ArgumentNullException>(worldService == null);
this.loggingService = loggingService;
this.worldService = worldService;
}
You can also invoke a callback, and provide a custom message.
public DefaultGame(ILoggingService loggingService, IWorldService worldService)
{
ExceptionFactory
.ThrowExceptionIf< ArgumentNullException>(loggingService == null, "Logging Service must not be null!")
.ElseDo(() => this.loggingService = loggingService);
ExceptionFactory
.ThrowExceptionIf< ArgumentNullException>(worldService == null, "World Service must not be null!")
.ElseDo(() => this.worldService = worldService);
}
If you need to do something more complex with your conditional check, you use a Func< bool>
ExceptionFactory
.ThrowExceptionIf< Exception>(
() =>
{
this.Worlds = worldService.GetAllWorlds();
return this.Worlds.Any();
},
"An empty world can not be used!"));
It also supports providing a custom exception factory method for use. Since some exceptions have additional parameters in the constructor, this can be really useful.
ExceptionFactory
.ThrowExceptionIf< ArgumentNullException>(
worldService == null,
() => new ArgumentNullException("worldService", "World Service must not be null!"));
We can also pass custom data in to the exception if we want.
ExceptionFactory
.ThrowExceptionIf< ArgumentNullException>(
worldService == null,
() => new ArgumentNullException("worldService", "World Service must not be null!"),
new KeyValuePair< string, string>("Member", "DefaultGame"),
new KeyValuePair< string, string>("MemberType", "Constructor"));
So how does the insides work? Let's take a look
The Factory
There are a total of four factory methods.
ThrowExceptionIf< TException>(Func< bool> predicate, string message = null, params KeyValuePair< string, string>[] data);
ThrowExceptionIf< TException>(Func< bool> predicate, Func< TException> exception, params KeyValuePair< string, string>[] data)
ThrowExceptionIf< TException>(bool condition, string message = null, params KeyValuePair< string, string>[] data)
ThrowExceptionIf< TException>(bool condition, Func< TException> exception, params KeyValuePair< string, string>[] data)
and one last method for adding data to the exception
AddExceptionData(Exception exception, params KeyValuePair< string, string>[] data)
Since we can build this in an overloaded fashion, we will just build the most complex method out, then let the rest of them piggy back on top of it.
public static ExceptionFactoryResult ThrowExceptionIf< TException>(bool condition, Func< TException> exception, params KeyValuePair< string, string>[] data) where TException : Exception, new()
{
if (condition)
{
return new ExceptionFactoryResult();
}
TException exceptionToThrow = exception();
AddExceptionData(exceptionToThrow, data);
throw exceptionToThrow;
}
This method is really straight forward, it requires a bool value to indicate if this exception must be instanced and thrown or not. If the condition is false, then we go ahead and invoke the Func< TException>
delegate. This returns the exception that we are to throw.
Next we take the optional exception data parameter and pass it to an AddExceptionData
method. That method will iterate over the param data and add it to the exception. We then finally throw the exception. Really straight forward.
The AddExceptionData
method looks like this:
public static void AddExceptionData(Exception exception, params KeyValuePair< string, string>[] data)
{
foreach (var exceptionData in data)
{
exception.Data.Add(exceptionData.Key, exceptionData.Value);
}
}
Now we can build out our overloads. The second most complex one is really easy to write. We just pass a delegate in that instances a new exception using the Activator
class.
public static ExceptionFactoryResult ThrowExceptionIf< TException>(bool condition, string message = null, params KeyValuePair< string, string>[] data) where TException : Exception, new()
{
return ThrowExceptionIf< TException>(
condition,
() => (TException)Activator.CreateInstance(typeof(TException), message),
data);
}
Next, we will provide support for delegate predicates.
public static ExceptionFactoryResult ThrowExceptionIf< TException>(Func< bool> predicate, Func< TException> exception, params KeyValuePair< string, string>[] data) where TException : Exception, new()
{
return ThrowExceptionIf< TException>(predicate(), exception, character, data);
}
public static ExceptionFactoryResult ThrowExceptionIf< TException>(Func< bool> predicate, string message = null, params KeyValuePair< string, string>[] data) where TException : Exception, new()
{
return ThrowExceptionIf< TException>(predicate(), message, character, data);
}
The last thing we need to do is support the method callbacks. This is done using the ExceptionFactoryResult
class that all of our factory methods return. This class has nothing more than a simple method that takes an Action and invokes it.
public class ExceptionFactoryResult
{
public void ElseDo(Action callback)
{
callback();
}
}
That's all there is to it!