Skip to content

Null Object

Last Updated on 27/12/2023

Null Object Design Pattern logo

Null Object é um design pattern comportamental baseado em herança que cria representações válidas de objetos null em um sistema, de modo a evitar que se retorne um null quando um objeto é esperado, por consequência, gerando null checks para evitar que exceções NullPointerException sejam lançadas e comportamentos inesperados sejam apresentados.

Introdução

É comum escrever métodos que retornem null em situações onde a informação requisitada não está presente ou algumas condições não são atendidas para executar um determinado trecho de código. Entretanto, as vezes este tipo de comportamento é mal documentado e acaba pegando outros desenvolvedores de surpresa quando estes decidem utilizar uma API, além disso, isto pode forçar tais desenvolvedores a escreverem inúmeros null checks a fim de evitar exceções de runtime.

De qualquer modo, o código da aplicação pode se tornar pouco coeso e pouco limpo, pois agora aquele trecho de código também precisa lidar com as situações onde um null é possível, tendo assim que tomar decisões que inicialmente não seriam de sua responsabilidade.

O design pattern Null Object vem para trabalhar neste problema, de um modo que basicamente, ao invés de simplesmente retornar um null onde um objeto da classe Foo era esperado, ele retorna um objeto que é subclasse de Foo em um estado válido para ser utilizado em tempo de execução, aderindo ao contrato de Foo.

Estrutura do Null Object

Imagine que tenhamos uma loja online, na qual consultamos o banco de dados buscando por descontos diários para serem aplicados nos produtos sendo vendidos. Para tanto, teremos uma classe Discount da qual iremos herdar para aplicarmos o design pattern Null Object.

Abaixo está o diagrama UML com sua representação:

Null Object UML representation

Algumas considerações importantes a respeito do diagrama:

  • CheckOut é somente um exemplo de classe cliente que irá usufruir das vantagens do design pattern, porém, poderia ser qualquer classe;
  • Os métodos da classe NullDiscount sobrescrevem os métodos da classe Discount aderindo ao seu contrato;
  • Não vejo qualquer problema em implementar este design pattern sobre uma interface ao invés de utilizar herança, porém é mais comum que ele seja utilizando em objetos de domínio, que por sua vez, acabam por ser classes.

Implementação

Abaixo estão as classes Java que implementam o pattern. Como de costume, alguns javadocs, comentários, getters, setters, construtores e sobrescritas de toString foram omitidos para fins de legibilidade. Por favor acesse o repositório no GitHub para obter a versão completa deste código.

Discount: a classe de domínio que será populada com as informações do desconto que encontrarmos (caso existam):

package com.bgasparotto.designpatterns.nullobject;

import java.math.BigDecimal;
import java.time.LocalDateTime;

public class Discount {
	private BigDecimal value;
	private LocalDateTime date;

	public BigDecimal getValue() {
		return value;
	}

	public LocalDateTime getDate() {
		return date;
	}

	// Setters.
}

NullDiscount: a classe que implementa o pattern baseado na classe Discount:

package com.bgasparotto.designpatterns.nullobject;

import java.math.BigDecimal;
import java.time.LocalDateTime;

public class NullDiscount extends Discount {

	@Override
	public BigDecimal getValue() {
		return BigDecimal.ZERO;
	}

	@Override
	public LocalDateTime getDate() {
		return LocalDateTime.now();
	}
}

CheckOut: a classe cliente que irá se beneficiar da implementação do pattern:

package com.bgasparotto.designpatterns.nullobject;

import java.math.BigDecimal;
import java.time.LocalDateTime;

public class CheckOut {
	private BigDecimal cartValue;
	private Discount discount;

	public CheckOut(BigDecimal cartValue) {
		this.cartValue = cartValue;
	}

	public void doCheckOut() {
		DiscountService discountService = new DiscountService();
		discount = discountService.findDiscount(LocalDateTime.now());

		// Aqui haveria um null check sem o pattern.
		BigDecimal discountValue = discount.getValue();
		cartValue = cartValue.subtract(discountValue);

		System.out.println("Final cart value is " + cartValue);

		// Aqui haveria um null check sem o pattern.
		System.out.println("You got a discount of " + discountValue);
	}

	public static void main(String[] args) {
		CheckOut checkOut = new CheckOut(new BigDecimal(1000));
		checkOut.doCheckOut();
	}
}

Por último, uma classe de serviço que não estava no diagrama porém será responsável por buscar e retornar eventuais descontos. Já que estamos discutindo a implementação do design pattern, irei mockar a “busca” no banco de dados:

package com.bgasparotto.designpatterns.nullobject;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.concurrent.ThreadLocalRandom;

public class DiscountService {

	public Discount findDiscount(LocalDateTime date) {
		
		// Vai até o banco de dados e busca os descontos de hoje.
		Discount discount = findMockDiscount(date);
		return discount;
	}

	private Discount findMockDiscount(LocalDateTime date) {
		
		// Gera um número aleatório e retorna null se ele for par.
		int random = ThreadLocalRandom.current().nextInt(0, 9 + 1);
		if (random % 2 == 0) {
			return null;
		}
		
		// Senão, retorna um objeto de desconto válido
		Discount discount = new Discount();
		discount.setValue(BigDecimal.TEN);
		discount.setDate(date);
		return discount;
	}
}

Discussão

Primeiro, note a declaração de valor padrão e o null check:

// Valor padrão caso o desconto seja null
BigDecimal discountValue = BigDecimal.ZERO;

// Null check sem o pattern.
if (discount != null) {
	discountValue = discount.getValue();
}
cartValue = cartValue.subtract(discountValue);

Podemos dizer que eles existem pois temos uma brecha de coesão aqui, a classe DiscountService deveria nos devolver um objeto de desconto, porém ao invés disto, ela pode retornar null caso o serviço de consulta de descontos esteja indisponível, passando o problema adiante de modo a deixar a nosso cargo tratar dele.

Agora imagine um sistema robusto com estes tipos de checagem em cada local onde for necessário obter um desconto? Podemos acabar cheios de código duplicado, pois já que um null check funcionou, a tendência é que acabemos por copiar e colar o mesmo null check nos outros trechos de código que estamos desenvolvendo, entretanto, como todos deveriam saber, quando se copia e cola algo em um código orientado a objetos, algo deu seriamente errado!

Mas e se fosse possível remover estes null checks? Para começar, nosso código já iria parecer mais limpo, então vamos colocar esse ideia em prática adotando-a como o primeiro passo para aplicarmos o pattern:

// Sem mais null checks
BigDecimal discountValue = discount.getValue();
cartValue = cartValue.subtract(discountValue);

No próximo passo, iremos centralizar o null check removido na classe DiscountService, deste modo, qualquer classe cliente poderá se beneficiar desta checagem.
Então, ao invés de:

Discount discount = findMockDiscount(date);
return discount;

Nós iremos retornar a implementação de nosso pattern caso o nosso método mock retorne null:

if (discount == null) {
	return new NullDiscount();
}
return discount;

A regra aqui é retornar a representação nula de nosso objeto já na primeira classe de negócio onde você retornaria um null, para evitar que estes nulls sejam propagados em modo “cascata” para suas classes clientes.

Finalmente, no Java 8 você pode utilizar Optional para se livrar do último null check:

Optional<Discount> optional = Optional.ofNullable(discount);
return optional.orElse(new NullDiscount());

Conclusão

Não se deixe enganar caso a diferença entre as versões com e sem pattern pareçam pequenas, pois lembre-se, este é apenas um exemplo em um sistema extremamente pequeno para fins educativos. Em sistemas reais, as classes tendem a serem muito maiores e com mais atributos que precisam ser testados para null, então você poderá se deparar com dezenas de linhas de código apenas para esta finalidade.

Além disso, nós aumentamos a coesão de nosso código ao atribuir a classe DiscountService a responsabilidade de lidar com valores null, centralizando a abordagem em apenas um lugar. Dito isto, futuras manutenções se tornarão mais simples pois sabemos que será preciso modificarmos apenas um local, sem precisarmos lidar com o código cliente diretamente.

Leave a Reply

Your email address will not be published. Required fields are marked *