Most object-oriented languages have a way of specifying the nonexistence of some value. In Scala and Java, for example, this could be the null
value that can be assigned to an object. Calling any method on an object that is null
would result in a NullPointerException
, hence developers should be careful and check whether there is such a possibility. These checks, however, could make the source code hard to follow and extend as developers should always be aware. This is where the null object design pattern is helpful. Its purpose is:
Using null objects removes the need to check whether something is set to null
or not. The code becomes much more readable and easy to understand and makes bug occurrence harder.
For the class diagram, let's imagine that we have a system that has to poll a queue for messages. Of course, this queue might not always have anything to offer, so it would return nulls. Instead of checking for null
, we could simply return special null objects that will have empty behavior. Let's show these message classes in a diagram:
Using the classes from the preceding diagram, whenever there is no number to print, we will return a NullMessage
object with empty behavior. In some cases, for optimization purposes, people could have the NullMessage
as a singleton instance.
Before we actually look at our code, we will note a few observations about the preceding diagram. It represents a classical case using the null object design pattern. However, nowadays, it is not really used this way in either Java or Scala. For example, Java now supports Optional
, which is used instead (assuming people use new versions of the language). In Scala, things are similar—we can and do use Option[Message]
instead of null objects. Additionally, we gain all the other nice features of Option
such as the ability to use them in pattern matching.
So, as mentioned previously, our code is not really going to use the hierarchy of the preceding class diagram. It just doesn't need it and it will be much simpler. Instead, we will be using Option[Message]
. First of all, let's see the Message
class definition:
case class Message(number: Int) { def print(): String = s"This is a message with number: $number." }
We mentioned that we will be polling a queue for messages and then displaying them. We have simulated a queue that is being randomly populated using a different thread in our application:
class DataGenerator extends Runnable { val MAX_VAL = 10 val MAX_TIME = 10000 private var isStop = false private val queue: ConcurrentLinkedQueue[Int] = new ConcurrentLinkedQueue[Int]() override def run(): Unit = { val random = new Random() while (!isStop) { Thread.sleep(random.nextInt(MAX_TIME)) queue.add(random.nextInt(MAX_VAL)) } } def getMessage(): Option[Message] = Option(queue.poll()).map { case number => Message(number) } def requestStop(): Unit = this.synchronized { isStop = true } }
The preceding code shows what will be run in a different thread. The queue will be populated with random values between 0
(inclusive) and 10
(exclusive) at random intervals. Then getMessage
can be called and whatever is in the queue can be read. Since it is possible for the queue to be empty, we return an Option
of Message
to the caller. It is probably worth mentioning that in Scala, Option(null)
returns None
. This is exactly what we took advantage of in the preceding code.
Let's see how everything comes together in our example:
object MessageExample { val TIMES_TO_TRY = 10 val MAX_TIME = 5000 def main(args: Array[String]): Unit = { val generator = new DataGenerator // start the generator in another thread new Thread(generator).start() val random = new Random() (0 to TIMES_TO_TRY).foreach { case time => Thread.sleep(random.nextInt(MAX_TIME)) System.out.println("Getting next message...") generator.getMessage().foreach(m => System.out.println(m.print())) } generator.requestStop() } }
The preceding program creates a generator and makes it run on a different thread. Then it randomly requests items from the generator and prints them if something is actually returned. Due to the use of random generators, the program will print different things every time. Here is an example run:
As you can see from our example and the preceding output, we never actually check for nulls and our code just doesn't do anything when the queue returns null
. This works really nicely in large projects and it makes the source code look really elegant and easy to understand.
Just for completeness, in real-life applications, code as in the previous example might not be a good idea. First of all, instead of calling sleep
on a thread, we can use timers and second, and if we want to create producer-consumer applications, we can use libraries such as
Akka (http://akka.io/), which allow us to do reactive programming and have really optimal code.
As you already saw, the null object design pattern is already incorporated in Scala (and newer versions of Java) through the use of Option
(Optional
in Java). This makes it really easy to use and shows the power of the language once more. Using null objects makes our code look much more readable and removes the need to take extra care when a value is null
. It also reduces the risk of bugs.
13.58.50.156