2926. Design Pattern - Null Object
Null 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