Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Migrated to Confluence 5.3

The Null Object Pattern involves using a special object place-marker object representing null. Typically, if you have a reference to null, you can't invoke reference.field or reference.method(). You receive the dreaded NullPointerException. The null object pattern uses a special object representing null, instead of using an actual null. This allows you to invoke field and method references on the null object. The result of using the null object should semantically be equivalent to doing nothing.

Simple Example

Suppose we have the following system:

Code Block
class Job {
    def salary
}

class Person {
    def name
    def Job job
}

def people = [
    new Person(name:'Tom', job:new Job(salary:1000)),
    new Person(name:'Dick', job:new Job(salary:1200)),
]

def biggestSalary = people.collect{ p -> p.job.salary }.max()
println biggestSalary

When run, this prints out 1200. Suppose now that we now invoke:

Code Block
people << new Person(name:'Harry')

If we now try to calculate biggestSalary again, we receive a null pointer exception.

To overcome this problem, we can introduce a NullJob class and change the above statement to become:

Code Block
class NullJob extends Job { def salary = 0 }

people << new Person(name:'Harry', job:new NullJob())
biggestSalary = people.collect{ p -> p.job.salary }.max()
println biggestSalary

This works as we require but it's not always the best way to do this with Groovy. Groovy's safe-dereference operator (?.) operator and null aware closures often allow Groovy to avoid the need to create a special null object or null class. This is illustrated by examining a groovier way to write the above example:

Code Block
people << new Person(name:'Harry')
biggestSalary = people.collect{ p -> p.job?.salary }.max()
println biggestSalary

Two things are going on here to allow this to work. First of all, max() is 'null aware' so that [300, null, 400].max() == 400. Secondly, with the ?. operator, an expression like p?.job?.salary will be equal to null if salary is equal to null, or if job is equal to null or if p is equal to null. You don't need to code a complex nested if ... then ... else to avoid a NullPointerException.

Tree Example

Consider the following example (inspired by this) where we want to calculate size, cumulative sum and cumulative product of all the values in a tree structure.

Our first attempt has special logic within the calculation methods to handle null values.

Code Block
class NullHandlingTree {
    def left, right, value
    
    def size() {
        1 + (left ? left.size() : 0) + (right ? right.size() : 0)
    }

    def sum() {
       value + (left ? left.sum() : 0) + (right ? right.sum() : 0)
    }

    def product() {
       value * (left ? left.product() : 1) * (right ? right.product() : 1)
    }
}

def root = new NullHandlingTree(
    value:2,
    left: new NullHandlingTree(
        value:3,
        right: new NullHandlingTree(value:4),
        left: new NullHandlingTree(value:5)
    )
)

println root.size()
println root.sum()
println root.product()

If we introduce the null object pattern (here by defining the NullTree class), we can now simplify the logic in the size(), sum() and product() methods. These methods now much more clearly represent the logic for the normal (and now universal) case. Each of the methods within NullTree returns a value which represents doing nothing.

Code Block
class Tree {
    def left = new NullTree(), right = new NullTree(), value
    
    def size() {
        1 + left.size() + right.size()
    }

    def sum() {
       value + left.sum() + right.sum()
    }

    def product() {
       value * left.product() * right.product()
    }
}

class NullTree {
    def size() { 0 }
    def sum() { 0 }
    def product() { 1 }
}

def root = new Tree(
    value:2,
    left: new Tree(
        value:3,
        right: new Tree(value:4),
        left: new Tree(value:5)
    )
)

println root.size()
println root.sum()
println root.product()

The result of running either of these examples is:

Code Block
4
14
120

Note: a slight variation with the null object pattern is to combine it with the singleton pattern. So, we wouldn't write new NullTree() wherever we needed a null object as shown above. Instead we would have a single null object instance which we would place within our data structures as needed.