DEV Community

Cover image for Understanding Design Patterns: Null Object
Carlos Caballero
Carlos Caballero

Posted on • Originally published at carloscaballero.io

Understanding Design Patterns: Null Object

There are 23 classic design patterns, which are described in the original book, Design Patterns: Elements of Reusable Object-Oriented Software. These patterns provide solutions to particular problems, often repeated in the software development.

In this article, I am going to describe what the Null-Object Pattern is; and how and when it should be applied. This pattern is not included in the classic pattern book, but it was first published in the Pattern Languages of Program and it is widely used to avoid complexity.

Null Object Pattern: Basic Idea

In object-oriented programming, a null object is an object with no referenced value or with defined neutral (“null”) behaviour. The null object design pattern describes the uses of such objects and their behaviour (or lack therefor). — Wikipedia

The main feature of this pattern is that this allows avoid complexity in our code. In most languages such as Java, C# or JavaScript the references may be null. Depending on our business logic checking the code can be needed to ensure they are not null before invoking any methods, because methods typically cannot be invoked on null references.

To sum up, the null object pattern allows us to avoid conditional complexity by using objects rather than primitive-types. The UML diagram of this pattern is the following one:

The AbstractObject class is an abstract class which defines the different operations that must be implemented in RealObject and the "null" or "default" Object (NullObject). The RealObject will do the operation for each real object while that NullObject will do nothing or may that you want to do a default operation in this object.

Null-Object Pattern: When To Use

  1. You need to add responsibilities to individual objects dynamically and transparently, that is, without affecting other objects.

  2. You need to add responsibilities that can be withdrawn at any moment.

Null Object Pattern: Advantages

The Null-Object Pattern has several advantages, summarised in the following points:

  • It defines class hierarchies consisting of real objects and null objects.

  • Null objects can be used in place of real objects when the object is expected to do nothing.

  • The client code is more simple because the conditional complexity is avoided. Clients use real and null collaborators uniformly.

Null Object pattern — Example 1: Saiyan’s World (Problem)

I will now show you how you can implement this pattern using JavaScript/TypeScript. Before applying the pattern, it is interesting to be aware of the problem you are trying to solve. Next, we will give context to our Example. Imagine we have a class called Saiyan that will allow us to model the attributes and methods of our dear Saiyan. This class implements an ISaiyan interface that clearly determines the characteristics that every object must satisfy in order to be a true Saiyan. A factory called SaiyanFactory is used to create Saiyan objects. This class abstracts us from where the Saiyan come from, can be generated from RAM, queries in a database or a complex algorithm for the manufacture of new objects.

Our problem as developers arises in the classes that act as a client and make use of our factory. In the following client code, we have invoked the getSaiyan method to obtain several Saiyan, specifically we have created Vegeta, Bob, Son Goku and Laura. I understand that readers know that the only Saiyan from the previous list are Vegeta and Son Goku; and therefore, both Bob and Laura cannot be manufactured as objects of the Saiyan type.

We always have to make a check that the object returned by the factory is not a null object because we are not sure that the factory always returns objects of the Saiyan type.

The final code has unnecessary conditional complexity because there are repetitive code fragments if-else on each of the objects found. I understand that this code snippet could be abstracted using a function but it would still be in the code.

Therefore, we obtain the following UML diagram.

The ISayian and Saiyan code associated is the following:

export interface ISaiyan {
  name: string;
  power: number;
}
/****/

import { ISaiyan } from './saiyan.interface';

export class Saiyan {
  protected name: string;
  protected power: number;

  constructor({ name, power }: ISaiyan) {
    this.name = name;
    this.power = power;
  }
  getName(): string {
    return this.name;
  }

  public toString(): string {
    return `${this.name} - ${this.power}`;
  }
}
Enter fullscreen mode Exit fullscreen mode

The code associated with the factory which is a database find mock is the following one:

import { Saiyan } from './saiyan.class';

export class SaiyanFactory {
  public saiyans = [
    { name: 'Son Goku', power: 1000 },
    { name: 'Son Gohan', power: 800 },
    { name: 'Vegeta', power: 950 },
  ];

  public getSaiyan(name: string): Saiyan | null {
    // Mock Database find
    for (const saiyan of this.saiyans) {
      if (saiyan.name === name) {
        return new Saiyan(saiyan);
      }
    }
    return null;
  }
}

Enter fullscreen mode Exit fullscreen mode

Finally, the code associated to the client where the conditional complexity is exponential due to null-objects from factory.

import { SaiyanFactory } from './saiyan-factory.class';

const saiyanFactory = new SaiyanFactory();
const saiyan1 = saiyanFactory.getSaiyan('Vegeta');
const saiyan2 = saiyanFactory.getSaiyan('Bob');
const saiyan3 = saiyanFactory.getSaiyan('Son Goku');
const saiyan4 = saiyanFactory.getSaiyan('Laura');

console.log('Saiyan');
if (saiyan1 !== null) {
  console.log(saiyan1.toString());
} else {
  console.log('Not Available in Customer Database');
}
if (saiyan2 !== null) {
  console.log(saiyan2.toString());
} else {
  console.log('Not Available in Customer Database');
}
if (saiyan3 !== null) {
  console.log(saiyan3.toString());
} else {
  console.log('Not Available in Customer Database');
}
if (saiyan4 !== null) {
  console.log(saiyan4.toString());
} else {
  console.log('Not Available in Customer Database');
}
Enter fullscreen mode Exit fullscreen mode

Null Object pattern — Example 1: Saiyan’s World (Solution)

The solution is to use a null-ojbect pattern. The new UML diagram using this pattern is shown below:

Let’s start with the end that is what we are interested in obtaining after applying the pattern. If you observe the client code, the factory from which the four requests of our Saiyan are made are kept. They are stored in variables so this helps us avoid making any verifications of whether the object is null before we perform on each Saiyan. In our example, we are using the toString method only to illustrate that a method that returns a string is going to be arranged.

Therefore, we have eliminated complexity from clients, and this has done thanks to a small change in our internal class structure. The factory instead of using only a Saiyan class from which the new Saiyan are generated, will create a simple inheritance (rigid composition) from this Saiyan class giving rise to two new classes RealSaiyan and NullSaiyan, transforming the Saiyan class in an abstract class.

The Saiyan class now defines the methods that all derived Saiyan classes must implement, the logic of a Saiyan found in the knowledge base will be implemented in the RealSaiyan class while the logic of the objects not found (null) or even if we want default behaviors to be implemented in the NullSaiyan class.

In this way, there will always be a behavior, even when they are not freeing the client from that complexity that does not apply.

We will now take a look at the code generated with the implementation of this pattern:

import { SaiyanFactory } from './saiyan-factory.class';

const saiyanFactory = new SaiyanFactory();
const saiyan1 = saiyanFactory.getSaiyan('Vegeta');
const saiyan2 = saiyanFactory.getSaiyan('Bob');
const saiyan3 = saiyanFactory.getSaiyan('Son Goku');
const saiyan4 = saiyanFactory.getSaiyan('Laura');

console.log('Saiyan');
console.log(saiyan1.toString());
console.log(saiyan2.toString());
console.log(saiyan3.toString());
console.log(saiyan4.toString());
Enter fullscreen mode Exit fullscreen mode

The code associated to the factory, which return two kind of objects, is the following one:

import { AbstractSaiyan } from './saiyan.class';
import { NullSaiyan } from './null-saiyan.class';
import { RealSaiyan } from './real-saiyan.class';

export class SaiyanFactory {
  public saiyans = [
    { name: 'Son Goku', power: 1000 },
    { name: 'Son Gohan', power: 800 },
    { name: 'Vegeta', power: 950 },
  ];

  public getSaiyan(name: string): AbstractSaiyan {
    for (const saiyan of this.saiyans) {
      if (saiyan.name === name) {
        return new RealSaiyan(saiyan);
      }
    }
    return new NullSaiyan();
  }
}

Enter fullscreen mode Exit fullscreen mode

The code associated with the AbstractSaiyan is the following:

export abstract class AbstractSaiyan {
  protected name: string;
  protected power: number;
  public abstract getName(): string;
  public abstract toString(): string;
}
Enter fullscreen mode Exit fullscreen mode

Finally, the code associated to each concrete class is the following ones:

import { AbstractSaiyan } from './saiyan.class';
import { Saiyan } from './saiyan.interface';

export class RealSaiyan extends AbstractSaiyan {
  constructor({ name, power }: Saiyan) {
    super();
    this.name = name;
    this.power = power;
  }

  getName(): string {
    return this.name;
  }
  toString(): string {
    return `${this.name} - ${this.power}`;
  }
}
Enter fullscreen mode Exit fullscreen mode
import { AbstractSaiyan } from './saiyan.class';

export class NullSaiyan extends AbstractSaiyan {
  public getName(): string {
    return 'Not Available in Saiyan Database';
  }
  toString(): string {
    return 'Not Available in Saiyan Database';
  }
}
Enter fullscreen mode Exit fullscreen mode

I have created several npm scripts that run the code's examples shown here after applying the null-ojbect pattern.

npm run example1-problem

npm run example1-solution-1

Conclusion

The null-object pattern can avoid conditional complexity in your projects.
This pattern allows you to configure the default behavior in the event that there is no object, resulting in not having to insistently check if an object is null or not.

This pattern uses simple inheritance to solve the problem that arises. However, this pattern is classified as a particular case of another pattern studied in this blog: Strategy Pattern.

Therefore, one could say that this pattern is using rigid composition (inheritance) to solve a problem that could be solved with composition but would cause more complexity than is necessary for the problem it solves. This is a good example that every “tool” we have as a developer must be used at the right time, and the most important thing in our trade is to know all the tools and when we should use them.

The most important thing is not to implement the pattern as I have shown, but to be able to recognise the problem which this specific pattern can resolve, and when you may or may not implement said pattern. This is crucial, since implementation will vary depending on the programming language you use.

More more more…

Top comments (2)

Collapse
 
jbristow profile image
Jon Bristow

Instead of a Null Object, you should go all-the-way and just use a monoid.

That way you won't have the tonally weird situation described above when you have a NullSaiyan and a Saiyan type in your hierarchy, but you'll gain the benefits of the Null Object Pattern...

To me, seeing a Maybe Saiyan (which can either be a Saiyan or None) makes more contextual sense, as I know this is an object that can be one or the other.

It also allows me to create things like:

/*
 * Adds two Maybe Point objects into a list.
 */
fun Option<Point>.plus(element: Option<Point>) = this.fold(
    ifEmpty = { element.map(::listOf) },
    ifSome = { a ->
        element.fold(
            ifEmpty = { listOf(a) },
            ifSome = { b -> listOf(a, b) }
        ).some()
    }
)

Using a monoid lets you move the definition to the handler functions instead of providing you with a place to hide functionality away to surprise later consumers.

Collapse
 
drmaquino profile image
Mariano Aquino

The whole idea of the pattern is to let the client not know whether it is dealing with actual objects or nulls. It is usually used with "for-each"s and it is yet another case of polymorphism.
Your idea sounds good if you wanted to let the client decide what to do on each type, which is not the use case of this pattern.
Of course, different scenarios require different approaches.