Beatles1692, forgive me, but I'll start with addressing your main concern of refactoring exception handling instead of jumping to the DSL part straight away.
In your question you say the following:
I am reviewing part of a code base, and I come to the exception handling part which is really messy. I would like to replace it with something more elegant.
which I assume is your main concern. So I'll try to provide you a guide on how to make it more elegant - actually the code snippet you provided not being elegant is not about it not being a DSL or fluent interfaces, but about design qualitities. If you have redundancy and coupling in your design, then creating a fluent interface on top of that redundancy and coupling will only make it a "prettier mess".
The answer will be a long one and I'll refer to some code qualities, so if you need further explanations, just let me know. Since there are many variables involved in making such decision (like cost of change, code ownership etc.) I'll try to provide you with the "cleanest" solution and then with one that requires least effort.
In situations such as these, it's good to apply the advice from Gang of Four, the authors of the classical Design Patterns. This advice is: "Encapsulate what varies". in your case, it's the failure handling is what varies, and the variation is based on the exception type. How would I apply it here?
First solution - refactor the smells out of the code completely
The first question I'd ask is this: are you free to modify the code that throws the exceptions? If so, I'd try to encapsulate not inside the code where you catch the exceptions, but inside the code that throws them. That might seem strange for you, but this might allow you to avoid redundancy. As it is, your code is currently coupled to a type of exception in two places.
The first place is where you throw it (you have to know which exception to throw) - greatly simplified, it might look like this:
if(someSituationTakesPlace())
{
throw new ExpType1();
}
else if(someOtherSituationTakesPlace()
{
throw new ExpType2();
}
etc. Of course, the conditions might be more complex, there might be multiple different objects and methods where you throw, but essentially, it always boils down to a series of choices like "In situation A, throw Exception X".
The second place where you have this mapping is when you catch the exception - you have to, again, go through a series of if-elses to find out what situation it was and then invoke some logic that would handle it.
To avoid this redundancy, I'd decide on the handling where you throw the exception - you should have all the information you need there. Thus, I'd first define an exception class like this:
public class Failure : Exception
{
IFailureHandling _handling;
public Failure(IFailureHandling handling)
{
//we're injecting how the failure should be handled
_handling = handling;
}
//If you need to provide additional info from
//the place where you catch, you can use parameter list of this method
public void Handle()
{
_handling.Perform();
}
}
Then, I'd make a factory that creates such exceptions, binding them, already at this point, with handlers. E.g.:
public class FailureFactory
{
IFailureHandling _handlingOfCaseWhenSensorsAreDown,
IFailureHandling _handlingOfCaseWhenNetworkConnectionFailed
public FailureFactory(
IFailureHandling handlingOfCaseWhenSensorsAreDown,
IFailureHandling handlingOfCaseWhenNetworkConnectionFailed
//etc.
)
{
_handlingOfCaseWhenSensorsAreDown
= handlingOfCaseWhenSensorsAreDown;
_handlingOfCaseWhenNetworkConnectionFailed
= handlingOfCaseWhenNetworkConnectionFailed;
//etc.
}
public Failure CreateForCaseWhenSensorsAreDamaged()
{
return new Failure(_handlingOfCaseWhenSensorsAreDown);
}
public Failure CreateForCaseWhenNetworkConnectionFailed()
{
return new Failure(_handlingOfCaseWhenNetworkConnectionFailed);
}
}
You would typically create just one such factory per whole system and do it in the place where you instantiate all long-running objects (typically there's one such place in an application), so, while instantiating the factory, you should be able to pass all the objects you want it to use through the constructor (funny as it is, this would create a very primitive fluent interface. Remember fluent interfaces are about readability and flow, not only putting.a.dot.every.method.call :-):
var inCaseOfSensorDamagedLogItToDatabaseAndNotifyUser
= InCaseOfSensorDamagedLogItToDatabaseAndNotifyUser(
logger, translation, notificationChannel);
var inCaseOfNetworkDownCloseTheApplicationAndDumpMemory
= new InCaseOfNetworkDownCloseTheApplicationAndDumpMemory(
memoryDumpMechanism);
var failureFactory = new FailureFactory(
inCaseOfSensorDamagedLogItToDatabaseAndNotifyUser,
inCaseOfNetworkDownCloseTheApplicationAndDumpMemory
);
This way, both the place where you throw the exception and where you catch it are decoupled from the handling logic - which is what varies in your issue! Thus, we have encapsulated what varies! Of course, you're free to provide more advanced fluent interface on top of this.
Now every place where you throw the exception would look like this:
if(sensorsFailed())
{
throw _failureFactory.CreateForCaseWhenSensorsAreDamaged();
}
And a place where you catch all these exceptions would look like this:
try
{
PerformSomeLogic();
}
catch(Failure failure)
{
failure.Handle();
}
This way, the only place where you'd know how to handle each failure case would be within the logic that creates an object of class FailureFactory.
Second solution - use handler
In case you don't own the code that's throwing the exceptions or it would be too costly or too risky to put in the solution outlined above, I'd use a Handler object that would look similarly to the FailureFactory, but instead of creating objects, it would execute the handling himself:
public class FailureHandlingMechanism
{
_handlers = Dictionary<Type, IFailureHandling>();
public FailureHandler(Dictionary<Type, IFailureHandling> handlers)
{
_handlers = handlers;
}
public void Handle(Exception e)
{
//might add some checking whether key is in dictionary
_handlers[e.GetType()].Perform();
}
}
Instantiating such handling mechanism would already give you a very primitive fluent interface:
var handlingMechanism = new HandlingMechanism(
new Dictionary<Type, IFailureHandling>()
{
{ typeof(NullPointerException), new LogErrorAndCloseApplication()}},
{ typeof(ArgumentException}, new LogErrorAndNotifyUser() }
};
If you wish for even more fluent and less noisy way of configuring such handling mechanism, you can create a builder around the HandlingMechanism that has methods for adding keys and values to dictionary and a method called Build() that created the object for you:
var handlingMechanismThatPerforms = new HandlingMechanismBuilder();
var logErrorAndCloseApplication = new LogErrorAndCloseApplication();
var logErrorAndNotifyUser = new LogErrorAndNotifyUser();
var handlingMechanism = handlingMechanismThatPerforms
.When<NullPointerException>(logErrorAndCloseApplication)
.When<ArgumentException>(logErrorAndNotifyUser)
.Build();
And that's it. Let me know if it helps you in any way!