beginner Por Mathias Paulenko

Patrón Null Object

Usa un objeto por defecto en lugar de referencias null para eliminar verificaciones de null y simplificar el código cliente. Un patrón behavioral para defaults más seguros.

Temas: design

Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.

Descripción General

El Patrón Null Object elimina las verificaciones de referencias null proveyendo un objeto “no-op” por defecto que implementa la misma interfaz que los objetos reales. En lugar de ramificar con if (user != null) en todas partes, los clientes interactúan con un NullUser que retorna defaults seguros como strings vacíos, conteos en cero o comportamiento no-op.

Este patrón previene el error de mil millones de dólares de las referencias null. En lugar de crashear con NullPointerException o dispersar verificaciones defensivas por toda la base de código, el null object maneja datos faltantes gracefulmente.

Cuándo Usar

Usa el Patrón Null Object cuando:

  • Un método puede no retornar nada, pero los callers esperan un objeto para interactuar
  • Quieres evitar verificaciones de null dispersas por todo el código cliente
  • Los datos faltantes tienen un comportamiento default sensato (lista vacía, balance cero, usuario guest)
  • Quieres tratar la ausencia de datos como un concepto de primera clase

Cuándo Evitar

  • Un valor faltante es verdaderamente excepcional y debería fallar rápido
  • El comportamiento default silenciosamente ocultaría bugs (ej., saltear verificaciones de seguridad)
  • No hay un default significativo para el caso ausente

Solución

Python

from abc import ABC, abstractmethod

class User(ABC):
    @abstractmethod
    def get_name(self) -> str:
        pass

    @abstractmethod
    def has_access(self, resource: str) -> bool:
        pass

    @abstractmethod
    def get_permissions(self) -> list:
        pass


class RealUser(User):
    def __init__(self, name, permissions=None):
        self.name = name
        self.permissions = permissions or []

    def get_name(self):
        return self.name

    def has_access(self, resource):
        return resource in self.permissions

    def get_permissions(self):
        return self.permissions


class NullUser(User):
    """Null object con comportamiento default seguro."""

    def get_name(self):
        return "Guest"

    def has_access(self, resource):
        return False

    def get_permissions(self):
        return []


# Uso
def find_user(user_id):
    # Búsqueda simulada
    if user_id == 1:
        return RealUser("Alice", ["reports", "settings"])
    return NullUser()  # No hay null, no hay crash

user = find_user(999)
print(user.get_name())          # Guest
print(user.has_access("admin")) # False
print(user.get_permissions())   # []

Java

interface User {
    String getName();
    boolean hasAccess(String resource);
    List<String> getPermissions();
}

class RealUser implements User {
    private final String name;
    private final List<String> permissions;

    public RealUser(String name, List<String> permissions) {
        this.name = name;
        this.permissions = permissions;
    }

    public String getName() { return name; }
    public boolean hasAccess(String resource) {
        return permissions.contains(resource);
    }
    public List<String> getPermissions() { return permissions; }
}

class NullUser implements User {
    public String getName() { return "Guest"; }
    public boolean hasAccess(String resource) { return false; }
    public List<String> getPermissions() { return List.of(); }
}

// Uso
public class UserService {
    public User findUser(int id) {
        if (id == 1) return new RealUser("Alice", List.of("reports"));
        return new NullUser();  // Siempre retorna un User válido
    }
}

JavaScript

class User {
  getName() { throw new Error('Abstract'); }
  hasAccess(resource) { throw new Error('Abstract'); }
  getPermissions() { throw new Error('Abstract'); }
}

class RealUser extends User {
  constructor(name, permissions = []) {
    super();
    this.name = name;
    this.permissions = permissions;
  }
  getName() { return this.name; }
  hasAccess(resource) { return this.permissions.includes(resource); }
  getPermissions() { return this.permissions; }
}

class NullUser extends User {
  getName() { return 'Guest'; }
  hasAccess() { return false; }
  getPermissions() { return []; }
}

// Uso
function findUser(id) {
  if (id === 1) return new RealUser('Alice', ['reports']);
  return new NullUser();
}

const user = findUser(999);
console.log(user.getName());          // Guest
console.log(user.hasAccess('admin')); // false

Explicación

El Patrón Null Object tiene tres partes:

  • Interfaz Abstracta (User): Define el contrato que todos los objetos implementan
  • Objeto Real (RealUser): La implementación normal con datos reales
  • Null Object (NullUser): Un objeto válido que retorna defaults seguros

Los clientes nunca verifican por null; tratan todos los objetos uniformemente.

Variantes

VarianteComportamiento DefaultEjemplo
Null LoggerLlamadas de logging son no-opLogger de producción que descarta output debug
Null CacheSiempre miss, nunca storeWrapper de cache para ambientes sin Redis
Null SubscriptionUnsubscribe es no-opEvent handler que ignora callbacks safe
Null MailerSilenciosamente descarta emailsMailer de desarrollo que imprime a consola

Lo que funciona

  • Retorna null objects desde factories y búsquedas en lugar de None o null
  • Haz null objects inmutables para que no puedan ser modificados accidentalmente
  • Loggea el uso de null objects en modo debug para detectar ausencias inesperadas
  • Usa capacidades del lenguaje como Optional de Java o tipos anulables de C# junto con null objects para APIs que modelan explícitamente la ausencia
  • Mantén el comportamiento del null object simple — lógica compleja en un null object es un code smell

Errores Comunes

  • Null objects con side effects sorprendentes como silenciosamente tragar errores o permitir acceso no autorizado
  • Olvidar implementar nuevos métodos de interfaz en el null object cuando la interfaz cambia
  • Usar null objects donde las excepciones son correctas — un payment processor faltante debería fallar, no retornar un no-op processor
  • Almacenar estado mutable en null objects causa bugs de estado compartido cuando la misma instancia null es reutilizada
  • Crear null objects para tipos primitivosNullInt retornando 0 puede ser semánticamente incorrecto; usa Optional<int> en su lugar

Ejemplos del Mundo Real

Java Collections

Collections.emptyList() retorna una lista null object que implementa List. El código puede iterar, chequear tamaño y llamar contains() sin verificaciones de null.

Logging Frameworks

El NOP logger de SLF4J es un null object que silenciosamente descarta statements de log cuando no hay binding configurado, previniendo NullPointerException en logger.info().

UI Components

El rendering condicional de React a menudo usa componentes vacíos o fragments como null objects — renderizar <></> en lugar de null evita layout shifts.

Temas Avanzados

Escenario: Null Object para Logger y Config

// Null Object: un objeto que implementa la interfaz pero no hace nada
interface Logger {
  info(msg: string): void;
  warn(msg: string): void;
  error(msg: string): void;
  debug(msg: string): void;
}

// Logger real
class ConsoleLogger implements Logger {
  info(msg: string) { console.log(`[INFO] ${msg}`); }
  warn(msg: string) { console.warn(`[WARN] ${msg}`); }
  error(msg: string) { console.error(`[ERROR] ${msg}`); }
  debug(msg: string) { console.log(`[DEBUG] ${msg}`); }
}

// Null Object: no hace nada, pero implementa la interfaz
class NullLogger implements Logger {
  info(_msg: string) {}
  warn(_msg: string) {}
  error(_msg: string) {}
  debug(_msg: string) {}
}

// Null Object para Config
interface AppConfig {
  get(key: string): string | undefined;
  getNumber(key: string): number | undefined;
  getBoolean(key: string): boolean;
}

class NullConfig implements AppConfig {
  get(_key: string): string | undefined { return undefined; }
  getNumber(_key: string): number | undefined { return undefined; }
  getBoolean(_key: string): boolean { return false; }
}

// Uso: el servicio no necesita null checks
class OrderService {
  constructor(
    private logger: Logger,
    private config: AppConfig
  ) {}

  process(order: Order) {
    this.logger.info(`Processing order ${order.id}`);
    const maxItems = this.config.getNumber("MAX_ITEMS") || 100;
    if (order.items.length > maxItems) {
      this.logger.warn(`Order ${order.id} exceeds max items`);
    }
    // Sin Null Object: if (this.logger) { this.logger.info(...) }
    // Con Null Object: this.logger.info(...) siempre funciona
  }
}

// En tests: usar NullLogger para silenciar output
const service = new OrderService(new NullLogger(), new NullConfig());
service.process(order); // No output, no errors

// En produccion: usar ConsoleLogger
const prodService = new OrderService(new ConsoleLogger(), envConfig);

Lecciones:

  • Null Object implementa la interfaz pero no hace nada
  • Elimina null checks: if (logger) logger.info() -> logger.info()
  • En tests, NullLogger silencia output sin cambiar el codigo
  • En produccion, ConsoleLogger loguea normalmente
  • El cliente no sabe si usa el objeto real o el null
  • Null Object vs Optional: Optional es un wrapper; Null Object es una implementacion

### Null Object vs Optional: cual uso?

Usa Null Object cuando tienes una interfaz y quieres una implementacion que no haga nada (Logger, Config, Notifier). Usa Optional cuando el valor puede estar ausente y necesitas expresarlo en el tipo (Optional<User>). Null Object es una implementacion completa de la interfaz. Optional es un wrapper que fuerza al cliente a manejar el caso ausente. Para dependencias inyectadas, Null Object. Para valores de retorno, Optional.

Preguntas frecuentes

Null Object es lo mismo que Optional?
No. Optional fuerza a los callers a manejar la ausencia explícitamente. Null Object oculta la ausencia detrás de llamadas a métodos normales. Usa Optional para APIs; Null Object para grafos de...
Los null objects pueden mantener estado?
No deberían. Un null object es conceptualmente stateless. Si mantiene estado, probablemente sea un objeto real con un nombre inusual.
Cómo testeo código que usa Null Object?
Inyecta el null object explícitamente en tests y aserta que los métodos retornan los defaults esperados. No se necesita framework de mocking.