© Jennifer M. Kohnke
Faultily faultless, icily regular, splendidly null, Dead perfection, no more.
—Lord Alfred Tennyson (1809–1892)
Consider the following code:
Employee e = DB.GetEmployee("Bob");
if (e != null && e.IsTimeToPay(today))
e.Pay();
We ask the database for an Employee
object named "Bob"
. The DB
object will return null
if no such object exists. Otherwise, it will return the requested instance of Employee
. If the employee exists and is owed payment we invoke the pay
method.
We’ve all written code like this before. The idiom is common because, in C-based languages, the first expression of the &&
is evaluated first, and the second is evaluated only if the first is true
. Most of us have also been burned by forgetting to test for null
. Common though the idiom may be, it is ugly and error prone.
We can alleviate the tendency toward error by having DB.GetEmployee
throw an exception instead of returning null
. However, try
/catch
blocks can be even uglier than checking for null
.
We can address these issues by using the NULL OBJECT pattern.1 This pattern often eliminates the need to check for null
, and it can help to simplify the code.
Figure 25-1 shows the structure. Employee
becomes an interface that has two implementations. EmployeeImplementation
, the normal implementation, contains all the methods and variables that you would expect an Employee
object to have. When it finds an employee in the database, DB.GetEmployee
returns an instance of Employee-Implementation
. NullEmployee
is returned only if DB.GetEmployee
cannot find the employee.
NullEmployee
implements all the methods of Employee
to do “nothing.” What “nothing” is depends on the method. For example, one would expect that IsTimeToPay
would be implemented to return false
, since it is never time to pay a NullEmployee
.
Thus, using this pattern, we can change the original code to look like this:
Employee e = DB.GetEmployee("Bob");
if (e.IsTimeToPay(today))
e.Pay();
This is neither error prone nor ugly. There is a nice consistency to it. DB.Get-Employee
always returns an instance of Employee
. That instance is guaranteed to behave appropriately, regardless of whether the employee was found.
Of course, in many cases, we’ll still want to know whether DB.GetEmployee
failed to find an employee. This can be accomplished by creating in Employee
a static readonly
variable that holds the one and only instance of NullEmployee
.
Listing 25-1 shows the test case for NullEmployee
. In this case, "Bob"
does not exist in the database. Note that the test case expects IsTimeToPay
to return false
. Note also that it expects the employee returned by DB.GetEmployee
to be Employee.NULL
.
Listing 25-1. EmployeeTest.cs (partial)
[Test]
public void TestNull()
{
Employee e = DB.GetEmployee("Bob");
if (e.IsTimeToPay(new DateTime()))
Assert.Fail();
Assert.AreSame(Employee.NULL, e);
}
The DB
class is shown in Listing 25-2. Note that, for the purposes of our test, the GetEmployee
method simply returns Employee.NULL
.
public class DB
{
public static Employee GetEmployee(string s)
{
return Employee.NULL;
}
}
The Employee
class is shown in Listing 25-3. Note that this class has a static
variable, NULL
, that holds the sole instance of the nested implementation of Employee
. NullEmployee
implements IsTimeToPay
to return false
and Pay
to do nothing.
using System;
public abstract class Employee
{
public abstract bool IsTimeToPay(DateTime time);
public abstract void Pay();
public static readonly Employee NULL =
new NullEmployee();
private class NullEmployee : Employee
{
public override bool IsTimeToPay(DateTime time)
{
return false;
}
public override void Pay()
{
}
}
}
Making NullEmployee
a private
nested class is a way to make sure that there is only a single instance of it. Nobody else can create other instances of the NullEmployee
. This is a good thing, because we want to be able to say such things as:
if (e == Employee.NULL)
This would be unreliable if it were possible to create many instances of the null employee.
Those of us who have been using C-based languages for a long time have grown accustomed to functions that return null
or 0
on some kind of failure. We presume that the return value from such functions needs to be tested. The NULL OBJECT pattern changes this. By using this pattern, we can ensure that functions always return valid objects, even when they fail. Those objects that represent failure do “nothing.”
[PLOPD3] Robert C. Martin, Dirk Riehle, and Frank Buschmann, eds. Pattern Languages of Program Design 3, Addison-Wesley, 1998.
18.225.255.134