Sometimes if you have a complex system, you'll want the equivalent of 'permanent' environment settings, where 'permanent' is within the context of your execution. The normal path for these things would be some sort of config setting, and perhaps constructor injection to get those settings to your worker bees.
Here's a technique to make it simpler to get such a setting deep into your code, without some of the clutter of injecting yet another dependency. Is this an anti-pattern? I'd love to hear feedback against such a thing; I'll briefly address some of the potential drawbacks I can think of off the top of my head after exploring the code.
First a business scenario. Lets say your library / application is talking to an unreliable system, however it's a necessary evil due to the fact that your company, Acme Lottery Predictions, is dependent on. Once a day, every day, you go out to every newspaper in America's web sites, and find out what last night's winning lottery numbers are. You then try to predict what the next night's numbers will be in your library through fancy number-crunching.
Now unfortunately some of those sites will be down, and some will decide that they just didn't get around to it that day, and your business needs to go on, with or without that data. So you write this library that can handle the exceptions (truly these are sometimes exceptions -- let's say a server 500 error, or just a domain name that can't resolve, can't ping, etc... any number of 'horrible, really bad, no good' things) gracefully by logging and moving on (and possibly retrying later).
But you want to test your code individually -- sometimes you want to throw exceptions while testing, sometimes you don't. Enter a global settings class.(Note: you’ll see this sometimes in third-party libraries to set things like License Keys)
public static LotteryRemoteConnectivitySettings
{
public static int NumberOfAttemptsBeforeGivingUp { get { return 5; } }
private static bool _doRethrowExceptionsAfterLogging = false;
public static bool DoRethrowExceptionsAfterLogging
{
get { return _doRethrowExceptionsAfterLogging; }
set { _doRethrowExceptionsAfterLogging = value;}
}
}
And we have a unit... (well, integration test, really -- since this test goes against remote systems, by definition it is NOT a unit test) test of our system, to represent our client.
[TestFixture]
public class TestsRemoteConnectivityToWisconsinJournal
{
[SetUp]
public void Setup()
{
LotteryRemoteConnectivitySettings.
DoRethrowExceptionsAfterLogging=true;
}
[Test]
[Category("Integration Tests")]
[Category("Remote Site Tests")]
public void
Test_Wisconsin_Journal_Has_Lottery_Numbers_For_Date()
{
var dateToRetrieve = DateTime.Now;
var lotteryNumberGetter =
new WisconsinJournalLotteryGetter();
var expected = true;
var actual =
lotteryNumberGetter.
CanGetLotteryNumbersFor(dateToRetrieve);
Assert.AreEqual(expected, actual);
}
}
Simply put, in our test fixture's setup, we just have to call that one property, and the behavior should be that if the setting is true, then wherever we were catching exceptions before, we should throw them now. And how to accomplish this?
public class WisconsinJournalLotteryGetter
: IGetLotteryNumbers
{
public bool CanGetLotteryNumbersFor
(DateTime dateToRetrieve)
{
var canGetNumbers = false;
try
{
canGetNumbers =
DoTheVoodooThatYouDoSoWell();
}
catch (Exception exc)
{
Log(exc);
if (LotteryRemoteConnectivitySettings.
DoRethrowExceptionsAfterLogging)
{
throw;
}
}
return canGetNumbers;
}
}
Now we have a default behavior of 'false' when an exception is thrown and it's not allowed to rethrow exceptions; if it IS allowed, it will just throw the exceptions as normal. Either way it gets logged properly. This is not a substitute for proper exception handling, however it does allow a mission-critical system to stay alive even when unreliable dependencies disappear.
You will always have to make the business decision of 'What does it mean when the data that I retrieve from multiple sources is incomplete or untrustworthy?' Along that vein, you will have to decide how to crunch that data or retry. The best solution is to learn from your logged data what causes your dependencies to collapse, however that is not always under your control.
Now as far as what is bad about this method of using a global setting, let's take a look.
1. It's a hidden dependency. It's not immediately clear that WisconsinJournalLotteryGetter has a dependency on LotteryRemoteConnectivitySettings.
2. It's coupled code. This one can at least be mitigated through factories. If you had a factory that returned you back an interface matching LotteryRemoteConnectivitySettings, then you could provide the static class sometimes, and perhaps other times read it from the registry, etc. That way WisconsinJournalLotteryGetter could be dependent on an ILotteryRemoteConnectivitySettings.
3. Exception handling not ideal -- well, there are other uses of this global settings code; so for the given topic, I'm going to say that's not really relevant here.
4. If you 'miss' using this setting in 1 of your 500 newspaper concretes, good luck finding that.
Questions / Comments are always welcome.