2926. Design Pattern - Null ObjectNull Object
Behavioral Pattern: Null Object Pattern.
1. Null Object Pattern
The Null Object pattern is used to encapsulate the absence of an object by providing a substitutable alternative that offers suitable default do nothing behavior. In short, a design where “nothing will come of nothing”.
Use the Null Object pattern when:
- an object requires a collaborator. The Null Object pattern does not introduce this collaboration–it makes use of a collaboration that already exists
- some collaborator instances should do nothing
- you want to abstract the handling of null away from the client
2. Example of Implementation
2.1 Interface
public interface Shape {
double area();
double perimeter();
void draw();
}
2.2 Classes
public class Circle implements Shape {
private final double radius;
public Circle (double radius) {
this.radius = radius;
}
@Override
public double area() {
// Area = πr^2
return Math.PI * Math.pow(radius, 2);
}
@Override
public double perimeter() {
// Perimeter = 2πr
return 2 * Math.PI * radius;
}
@Override
public void draw() {
System.out.println("Drawing Circle with area: " + area() + " and perimeter: " + perimeter());
}
}
public class Rectangle implements Shape {
private final double width;
private final double height;
public Rectangle (double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
// A = w * h
return width * height;
}
@Override
public double perimeter() {
// P = 2(w + h)
return 2 * (width + height);
}
@Override
public void draw() {
System.out.println("Drawing Rectangle with area: " + area() + " and perimeter: " + perimeter());
}
}
public class Triangle implements Shape {
private final double a;
private final double b;
private final double c;
public Triangle (double a, double b, double c) {
this.a = a;
this.b = b;
this.c = c;
}
@Override
public double area() {
// Using Heron's formula:
// Area = SquareRoot(s * (s - a) * (s - b) * (s - c))
// where s = (a + b + c) / 2, or 1/2 of the perimeter of the triangle
double s = (a + b + c) / 2;
return Math.sqrt(s * (s - a) * (s - b) * (s - c));
}
@Override
public double perimeter() {
// P = a + b + c
return a + b + c;
}
@Override public void draw() {
System.out.println("Drawing Triangle with area: " + area() + " and perimeter: " + perimeter());
}
}
2.3 Problematic Usage
public class ShapeFactory {
public static Shape createShape(String shapeType) {
Shape shape = null;
if ("Circle".equalsIgnoreCase(shapeType)) {
shape = new Circle(3);
} else if ("Rectangle".equalsIgnoreCase(shapeType)) {
shape = new Rectangle(2, 4);
} else if ("Triangle".equalsIgnoreCase(shapeType)) {
shape = new Triangle(3, 4, 5);
} // else return null
return shape;
}
}
When client using this factory to get shape instance, null-check is required if it returns null object. Otherwise, NullPointerException occurs.
public class ShapeProcessor {
String[] shapeTypes = new String[] { "Circle", "Triangle", "Rectangle", null};
public ShapeProcessor () {
}
public void process() {
for (String shapeType : shapeTypes) {
Shape shape = ShapeFactory.createShape(shapeType);
if (shape != null) { // null-check is required if factory returns null object
System.out.println("Shape area: " + shape.area());
System.out.println("Shape Perimeter: " + shape.perimeter());
shape.draw();
System.out.println();
}
}
}
}
2.4 Implementation with NullObject Pattern
Create ‘null’ class as default shape.
public class NullShape implements Shape {
public NullShape () {}
@Override
public double area() {
return 0.0d;
}
@Override
public double perimeter() {
return 0.0d;
}
@Override
public void draw() {
System.out.println("Null object can't be drawn");
}
}
Factory can now return the null object.
public class ShapeFactory {
public static Shape createShape(String shapeType) {
Shape shape = null;
if ("Circle".equalsIgnoreCase(shapeType)) {
shape = new Circle(3);
} else if ("Rectangle".equalsIgnoreCase(shapeType)) {
shape = new Rectangle(2, 4);
} else if ("Triangle".equalsIgnoreCase(shapeType)) {
shape = new Triangle(3, 4, 5);
} else {
shape = new NullShape();
}
return shape;
}
}
Now, client doesn’t need the null check.
public class ShapeProcessor {
String[] shapeTypes = new String[] { "Circle", "Triangle", "Rectangle", null};
public ShapeProcessor () {
}
public void process() {
for (String shapeType : shapeTypes) {
Shape shape = ShapeFactory.createShape(shapeType);
// no null-check required since shape factory always creates shape objects
System.out.println("Shape area: " + shape.area());
System.out.println("Shape Perimeter: " + shape.perimeter());
shape.draw();
System.out.println();
}
}
}
Output.
Shape area: 28.274333882308138
Shape Perimeter: 18.84955592153876
Drawing Circle with area: 28.274333882308138 and perimeter: 18.84955592153876
Shape area: 6.0
Shape Perimeter: 12.0
Drawing Triangle with area: 6.0 and perimeter: 12.0
Shape area: 8.0
Shape Perimeter: 12.0
Drawing Rectangle with area: 8.0 and perimeter: 12.0
Shape area: 0.0
Shape Perimeter: 0.0
Null object cant be drawn