When your program starts running in production, the conditions and cases you thought impossible will happen, period. In reality, it's impossible to ensure that your code will have optimal running conditions all the time. As a developer, you'd like to ensure your projects will be able to cope with the imperfections of the real world. That is why techniques like defensive programming will always have a place in your toolbox.
This approach is the programming equivalent of defensive driving. In defensive driving, you take the responsibility of anticipating risky conditions and mistakes made by other drivers. In other words, you take responsibility for protecting yourself from other driver's mistakes and bad driving conditions.
Defensive programming, by comparison, requires you to recognize that bad things will happen to your program and design accordingly. You must anticipate and prepare for cases such as data corruption, connection timeouts and passing wrong data into a function. By deliberately handling cases that "will never happen" your code will be prepared to fare better under those circumstances.
Pre-conditions are one of the most widely spread forms of defensive programming. They guarantee that a method can be executed only when some requirements are met. Here's a typical example:
- Code: Select all
public void CreateAppointment(DateTime dateTime)
{
if (dateTime.Date < DateTime.Now.AddDays(1).Date)
throw new ArgumentException("Date is too early");
if (dateTime.Date > DateTime.Now.AddMonths(1).Date)
throw new ArgumentException("Date is too late");
/* Create an appointment */
}
Writing code like this is a good practice as it allows you to quickly react to any unexpected situations, therefore adhering to the .
if (controller == null)
throw new IllegalArgumentException("Arg controller is null"); // unnecessary!
controller.doSomething(); // it will throw NullPointerException, so the problem will be revealed - why to write extra code?switch (custStatus) {
case CustStatus.ACTIVE:
[process active customers]
break;
case CustStatus.PENDING:
[process pending customers]
break;
}switch (custStatus) {
case CustStatus.ACTIVE:
[process active customers]
break;
case CustStatus.PENDING:
[process pending customers]
break;
case CustStatus.INACTIVE:
case CustStatus.DELETED:
// do nothing
break;
default:
throw new Exception ("Invalid customer status " + custStatus + ".");
}if (isCarMode)
[process cars]
else
[process motorcycles]switch (currEntity) {
case TransportType.CAR:
[process cars]
break;
case TransportType.MOTORCYCLE:
[process motorcycles]
break;
default:
throw new Exception ("Invalid entity " + currEntity + ".");
}if (controller != null)
controller.doSomething();controller.doSomething();I recently asked a co-worker why a certain check was being done, and he answered with a shrug and said, "Just to be safe." Over my career, I've seen a lot of code written just to be safe. I've written a lot of that code myself! If I wasn't sure if I could rely on something, I'd add a safety check to prevent it from throwing an exception. At a glance, there doesn't appear to be a problem. But you haven't patched the hole and you haven't fixed the bug. Instead of an easy-to-trace exception, you have unusable values - bad data - infiltrating your program.
try
{
DoSomething();
}
catch (Exception ex)
{
Logger.Log(ex);
}try
{
DoSomething();
}
catch (Exception ex)
{
Logger.Log(ex);
throw;
}