Pattern 8Replacing Null Object

Intent

To avoid scattering null checks throughout our code by encapsulating the action taken for null references into a surrogate null object

Overview

A common way to represent the lack of a value in Java is to use a null reference. This leads to a lot of code that looks like so:

 
if​(null == someObject){
 
// default null handling behavior
 
}​else​{
 
someObject.someMethod()
 
}

This style leads to scattering null handling logic throughout our code, often repeating it. If we forget to check for null it may lead to a program crashing NullPointerException, even if there is a reasonable default behavior that can handle the lack of a value.

A common solution to this is to create a singleton null object that has the same interface as our real objects but implements our default behavior. We can then use this object in place of null references.

The two main benefits here are these:

  1. We can avoid scattering null checks throughout our code, which keeps our code clean and easier to read.

  2. We can centralize logic that deals with handling the absence of a value.

Using Null Object has its trade-offs, however. Pervasive use of the pattern means that your program probably won’t fail fast. You may generate a null object due to a bug and not know until much later in the program’s execution, which makes it much harder to track down the source of the bug.

In Java, I generally use Null Object judiciously when I know that there’s a good reason why I may not have a value for something and use null checks elsewhere. The difference between these two situations can be subtle.

For instance, let’s imagine we’re writing part of a system that looks up a person by a generated, unique ID. If the IDs are closely related to the system we’re writing and we know that every lookup should succeed and return a person, I’d stick with using null references. This way, if something goes wrong and we don’t have a person, we fail fast and don’t pass the problem on.

However, if the IDs aren’t closely related to our program, I’d probably use Null Object. Say, for instance, that the IDs are generated by some other system and imported into ours via a batch process, which means that there’s some latency between when the ID is created and when it becomes available to our system. In this case, handling a missing ID would be part of our program’s normal operation, and I’d use Null Object to keep the code clean and avoid extraneous null checks.

The functional replacements we examine will explore these tradeoffs.

Functional Replacement

We’ll examine a few different approaches here. In Scala, we’ll take advantage of static typing and Option typing to replace null object references. In Clojure, we’ll primarily focus on Clojure’s treatment of nil, but we’ll also touch on Clojure’s optional static typing system, which provides us with an Option much like Scala’s.

In Scala

We have null references in Scala just as we do in Java; however, it’s not common to use them. Instead we can take advantage of the type system to replace both null references and Null Object. We’ll look at two container types, Option and Either. The first, Option, lets us indicate that we may not have a value in a type-safe manner. The second, Either, lets us provide a value when we’ve got one and a default or error value when we don’t.

Let’s take a closer look at Option first. Option types are containers, much like a Map or a Vector, except they can only hold one element at most. Option has two important subtypes: Some, which carries a value, and the singleton object None, which does not. In the following code, we create a Some[String] that carries the value "foo" and a reference to None:

 
scala>​ def aSome = Some("foo")
 
aSome: Some[java.lang.String]
 
 
scala>​ def aNone = None
 
aNone: None.type

Now we can work with our Option instances in a variety of ways. Perhaps the simplest is the getOrElse method. The getOrElse method is called with a single argument, a default value. When called on an instance of Some, the carried value is returned; when called on None the default value is returned. The following code demonstrates this:

 
scala>​ aSome.getOrElse("default value")
 
res0: java.lang.String = foo
 
 
scala>​ aNone.getOrElse("default value")
 
res1: java.lang.String = default value

When working with Option, it’s cleanest to treat a value as another container type. For example, if we need to do something to a value inside an Option, we can use our old friend map, as in the following code:

 
scala>​ aSome.map((s) => s.toUpperCase)
 
res2: Option[java.lang.String] = Some(FOO)

We’ll examine some more-sophisticated ways of working with Option in the code samples.

One final note on Option: In its simplest form, it can be used much as we’d use a null check in Java, though there are more powerful ways to use it. However, even in this simplest form, there’s one major difference.

Option is part of the type system, so if we use it consistently we know exactly in which parts of our code we may have to deal with the lack of a value or a default value. Everywhere else we can write code safe in the knowledge that we’ll have a value.

In Clojure

In Clojure, we don’t have the Option typing that Scala’s static type system provides us. Instead, we’ve got nil, which is equivalent to Java’s null at the bytecode level. However, Clojure provides several convenient features that make it much cleaner to deal with the lack of a value using nil and that give us many of the same benefits we get with Null Object.

First up, nil is treated the same as false in Clojure. Combined with a pervasive use of expressions, this makes it much simpler to do a nil check in Clojure than it is to check for null in Java, as the following code demonstrates:

 
=> (if nil "default value" "real value")
 
"real value"

Second, the functions that we use to get values of our Clojure’s composite data structures provide a way to get a default value if the element we’re trying to retrieve isn’t present. Here we use the get method to try to retrieve the value for :foo from an empty map, and we get back our passed-in default value instead:

 
=> (get {} :foo "default value")
 
"default value"

The lack of a value for a key is distinct from a key that has the value of nil, as this code demonstrates:

 
=> (get {:foo nil} :foo "default value")
 
nil

Let’s dig into some code samples!

Sample Code: Default Values

We’ll start by looking at how we’d use Null Object as a default when we don’t get back a value from a map lookup. In this example, we’ll have a map full of people keyed off of an ID. If we don’t find a person for a given ID, we need to return a default person with the name “John Doe.”

Classic Java

In classic Java, we’ll create a Person interface with two subclasses, RealPerson and NullPerson. The first, RealPerson, allows us to set a first and last name, while NullPerson has them hardcoded to "John" and "Doe".

If we get a null back when we try to get a person by ID, we return an instance of NullPerson; otherwise we use the RealPerson we got out of the map. The following code sketches out this approach:

JavaExamples/src/main/java/com/mblinn/oo/nullobject/PersonExample.java
 
public​ ​class​ PersonExample {
 
private​ ​Map​<​Integer​, Person> people;
 
 
public​ PersonExample() {
 
people = ​new​ ​HashMap​<​Integer​, Person>();
 
}
 
 
public​ Person fetchPerson(​Integer​ id) {
 
Person person = people.get(id);
 
if​ (null != person)
 
return​ person;
 
else
 
return​ ​new​ NullPerson();
 
}
 
// Code to add/remove people
 
 
public​ Person buildPerson(​String​ firstName, ​String​ lastName){
 
if​(null != firstName && null != lastName)
 
return​ ​new​ RealPerson(firstName, lastName);
 
else
 
return​ ​new​ NullPerson();
 
}
 
}

Let’s see how we can use Scala’s Option to eliminate the explicit null check we need to do in Java.

In Scala

In Scala, the get on Map doesn’t return a value directly. If the key exists, the value is returned wrapped in a Some, otherwise a None is returned.

For instance, in the following code we create a map with two integer keys, 1 and 2, and String greetings as values. When we try to fetch either of them using get, we get back a String wrapped in a Some. For any other key, we get back a None.

 
scala>​ def aMap = Map(1->"Hello", 2->"Aloha")
 
aMap: scala.collection.immutable.Map[Int,java.lang.String]
 
 
scala>​ aMap.get(1)
 
res0: Option[java.lang.String] = Some(Hello)
 
 
scala>​ aMap.get(3)
 
res1: Option[java.lang.String] = None

We could work with the Option type directly, but Scala provides a nice shorthand that lets us get back a default value directly from a map, getOrElse. In the following REPL output, we use it to attempt to fetch the value for the key 3 from the map. Since it’s not there, we get back our default value instead.

 
scala>​ aMap.getOrElse(3, "Default Greeting")
 
res3: java.lang.String = Default Greeting

Now let’s see how we can use this handy feature to implement our person-fetching example. Here we’re using a trait as the base type for our people, and we’re using case classes for the RealPerson and NullPerson. We can then use an instance of NullPerson as the default value in our lookup. The following code demonstrates this approach:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/nullobject/Examples.scala
 
case​ ​class​ Person(firstName: ​String​=​"John"​, lastName: ​String​=​"Doe"​)
 
val​ nullPerson = Person()
 
 
def​ fetchPerson(people: Map[​Int​, Person], id: ​Int​) =
 
people.getOrElse(id, nullPerson)

Let’s define some test data so we can see this approach at work:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/nullobject/Examples.scala
 
val​ joe = Person(​"Joe"​, ​"Smith"​)
 
val​ jack = Person(​"Jack"​, ​"Brown"​)
 
val​ somePeople = Map(1 -> joe, 2 -> jack)

Now if we use fetchPerson on a key that exists, it’s returned; otherwise our default person is returned:

 
scala>​ fetchPerson(somePeople, 1)
 
res0: com.mblinn.mbfpp.oo.nullobject.Examples.Person = Person(Joe,Smith)
 
 
scala>​ fetchPerson(somePeople, 3)
 
res1: com.mblinn.mbfpp.oo.nullobject.Examples.Person = Person(John,Doe)

Now let’s take a look at how we can accomplish this in Clojure.

In Clojure

When we try to look up a nonexistent key from a map in Clojure, nil is returned.

 
=> ({} :foo)
 
nil

Clojure provides another way to look up keys from a map, the get function, which lets us provide an optional default value. The following REPL snippet shows a simple example of get in action.

 
=> (get :foo {} "default")
 
"default"

To write our person lookup example in Clojure, all we need to do is define a default null-person. We then pass it into get as a default value when we try to do our lookup, as the following code and REPL output demonstrates:

ClojureExamples/src/mbfpp/oo/nullobject/examples.clj
 
(​def​ null-person {:first-name ​"John"​ :last-name ​"Doe"​})
 
(​defn​ fetch-person [people id]
 
(​get​ id people null-person))
 
=> (def people {42 {:first-name "Jack" :last-name "Bauer"}})
 
#'mbfpp.oo.nullobject.examples/people
 
=> (fetch-person 42 people)
 
{:last-name "Bauer", :first-name "Jack"}
 
=> (fetch-person 4 people)
 
{:last-name "Doe", :first-name "John"}

The code in this example deals with a basic use of Null Object as a default value at lookup time. Next up, let’s take a look at how we’d handle working with Null Object and its replacements when the time comes to modify them.

Sample Code: Something from Nothing

Let’s take a look at our person example from a different angle. This time, instead of looking up a person that may not exist, we want to create a person only if we’ve got a valid first and last name. Otherwise, we want to use a default.

Classic Java

In Java, we’ll use the same null object we saw in Classic Java. If we have both a first and last name available to use, we’ll use a RealPerson; otherwise we’ll use a NullPerson.

To do this, we write a buildPerson that takes a firstName and a lastName. If either is null, we return a NullPerson; otherwise we return a RealPerson built with the passed-in names. The following code outlines this solution:

JavaExamples/src/main/java/com/mblinn/oo/nullobject/PersonExample.java
 
public​ Person buildPerson(​String​ firstName, ​String​ lastName){
 
if​(null != firstName && null != lastName)
 
return​ ​new​ RealPerson(firstName, lastName);
 
else
 
return​ ​new​ NullPerson();
 
}

This approach allows us to minimize the surface area of our code where we need to deal with null, which helps cut down on surprise null pointers. Now let’s see how we can accomplish the same in Scala without needing to introduce an extraneous null object.

In Scala

Our Scala approach to this problem will take advantage of Option instead of creating a special Null Object type. The firstName and lastName we pass into buildPerson are Option[String]s, and we return an Option[Person].

If both firstName and lastName are Some[String], then we return a Some[Person]; otherwise we return a None. The right way to do this in Scala is to treat the Options as we would treat any other container, such as a Map or a Vector.

Earlier we saw a simple example of using the map method on an instance of Some. Let’s look at how we’d use Scala’s most powerful sequence manipulation tool, the sequence comprehensions we introduced in Sample Code: Sequence Comprehensions, to manipulate Option types.

First, let’s get some test data into our REPL. In the following snippet, we define a simple vector and a few option types:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/nullobject/Examples.scala
 
def​ vecFoo = Vector(​"foo"​)
 
def​ someFoo = Some(​"foo"​)
 
def​ someBar = Some(​"bar"​)
 
def​ aNone = None

As we can see in the following code, manipulating a Some looks much like manipulating a Vector with a single value in it:

 
scala>​ for(theFoo <- vecFoo) yield theFoo
 
res0: scala.collection.immutable.Vector[java.lang.String] = Vector(foo)
 
scala>​ for(theFoo <- someFoo) yield theFoo
 
res1: Option[java.lang.String] = Some(foo)

The real power of using a for comprehension to work with Option comes in when we’re working with multiple Options at a time. We can use multiple generators, one for each option, to get at the values in each. In the following code, we use this technique to pull the strings out of someFoo and someBar and put them into a tuple, which we then yield:

 
scala>​ for(theFoo <- someFoo; theBar <- someBar) yield (theFoo, theBar)
 
res2: Option[(java.lang.String, java.lang.String)] = Some((foo,bar))

When working with options in this fashion, if any of the generators produces a None, then the value of the entire expression is a None. This gives us a clean syntax for working with Some and None:

 
scala>​ for(theFoo <- someFoo; theNone <- aNone) yield (theFoo, theNone)
 
res3: Option[(java.lang.String, Nothing)] = None

We can now apply this to our person-building example pretty simply. We use two generators in our for comprehensions, one for the firstName and one for the lastName. We then yield a Person. The for comprehension wraps that up inside of an Option, and we use getOrElse to get at it or use a default. The following code demonstrates this approach:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/nullobject/Examples.scala
 
def​ buildPerson(firstNameOption: ​Option​[​String​], lastNameOption: ​Option​[​String​]) =
 
(​for​(
 
firstName <- firstNameOption;
 
lastName <- lastNameOption)
 
yield​ Person(firstName, lastName)).getOrElse(Person(​"John"​, ​"Doe"​))

Here we can see it in action:

 
scala>​ buildPerson(Some("Mike"), Some("Linn"))
 
res4: com.mblinn.mbfpp.oo.nullobject.Examples.Person = Person(Mike,Linn)
 
 
scala>​ buildPerson(Some("Mike"), None)
 
res5: com.mblinn.mbfpp.oo.nullobject.Examples.Person = Person(John,Doe)

Let’s finish up the example by seeing how to handle person-building in Clojure.

In Clojure

In Clojure, our person-building example boils down to a simple nil check. We pass first-name and last-name into our build-person function. If they’re both not-nil, we use them to create a person; otherwise we create a default person.

Clojure’s treatment of nil as a “falsey” value makes this convenient to do, but otherwise it’s very similar to our Java approach. The code follows:

ClojureExamples/src/mbfpp/oo/nullobject/examples.clj
 
(​defn​ build-person [first-name last-name]
 
(​if​ (​and​ first-name last-name)
 
{:first-name first-name :last-name last-name}
 
{:first-name ​"John"​ :last-name ​"Doe"​}))

Here it produces a real person and a default person:

 
=> (build-person "Mike" "Linn")
 
{:first-name "Mike", :last-name "Linn"}
 
=> (build-person "Mike" nil)
 
{:first-name "John", :last-name "Doe"}

Discussion

The idiomatic approach to handling the lack of a value in Clojure versus Scala is very different. The difference comes down to Scala’s static type system and Clojure’s dynamic one. Scala’s static type system and type parameters make the Option type possible.

The tradeoffs that Scala and Clojure make here mirror the general tradeoffs between static and dynamic typing. With Scala’s approach, the compiler helps to ensure that we’re properly handling nothing at compile time, though we have to be careful not to let Java’s null creep into our Scala code.

With Clojure’s approach, we’ve got the possibility for null pointers just about anywhere, just as in Java. We need to be more careful that we’re handling them appropriately, or we risk runtime errors.

My preference is to take care of all my nothing handling at the outermost layer of my code, whether I’m using Scala’s Option typing or the null/nil that Java and Clojure share. For instance, if I’m querying a database for a person who may or may not exist, I prefer to check for his/her existence only once: when we attempt to pull it back from the database. Then I use the techniques outlined in this pattern to create a default person if necessary. This allows the rest of my code to avoid doing null checks or to deal with Option typing. I’ve found that Scala’s approach to Option typing makes it much easier to write programs in this style, because it forces us to explicitly deal with the lack of a value whenever we might not have one and to assume that we’ll have a value everywhere else.

For Further Reading

Pattern Languages of Program Design 3 [MRB97]Null Object

Refactoring: Improving the Design of Existing Code [FBBO99]Introduce Null Object

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.113.163