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
You need to add responsibilities to individual objects dynamically and transparently, that is, without affecting other objects.
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}`;
}
}
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;
}
}
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');
}
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());
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();
}
}
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;
}
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}`;
}
}
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';
}
}
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.