Un Null Object es un objeto que representa la ausencia y conserva el contrato esperado por el cliente. Puede implementar un comportamiento neutro, como un logger que descarta los mensajes.
El problema es cotidiano. Tienes un método que devuelve un objeto. A veces devuelve el objeto, y a veces devuelve null.
El cliente que llama a ese método vive con miedo:
// ❌ El miedo constante al null
ILogger logger = ObtenerLogger();
if (logger != null) // <--- Ruido defensivo
{
logger.Log("Hola mundo");
}Si olvidas ese if, tu aplicación explota. Si tienes 500 líneas de log en tu app, tienes 500 if ensuciando el código.
En lugar de devolver null, podemos devolver una implementación neutral. No siempre tiene que “no hacer nada”: también puede devolver valores seguros o representar un usuario invitado.
La Filosofía: Una caja vacía sigue siendo una caja
Imagina que pides una caja.
- Situación null: el repartidor no llega; no existe una caja que puedas abrir.
- Situación Null Object: recibes una caja vacía. La operación de abrir sigue siendo válida, aunque no produzca contenido.
El Null Object es un objeto real que implementa la interfaz correcta, pero sus métodos tienen el cuerpo vacío.
Implementación en C#
Vamos a limpiar un sistema de Logging.
La Interfaz
public interface ILogger
{
void Log(string mensaje);
void LogError(string error);
}La Implementación Real
public class ConsoleLogger : ILogger
{
public void Log(string mensaje)
{
Console.WriteLine($"[INFO] {mensaje}");
}
public void LogError(string error)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"[ERROR] {error}");
Console.ResetColor();
}
}El Objeto Nulo (Null Object)
Esta es la clave. Es una clase “tonta”. No hace nada. Cero efectos secundarios.
public class NullLogger : ILogger
{
// Método vacío: "Hacer nada" es una implementación válida
public void Log(string mensaje) { }
public void LogError(string error) { }
}El Cliente y la Fábrica
Supongamos que tenemos un servicio que a veces necesita loguear y a veces no (dependiendo de la configuración).
Sin Null Object (Malo):
Tendríamos que devolver null si el log está desactivado, obligando al cliente a chequear siempre.
Con Null Object (Bueno):
public class LoggerFactory
{
public static ILogger GetLogger(bool habilitado)
{
if (habilitado)
{
return new ConsoleLogger();
}
else
{
// En lugar de devolver null, devolvemos el objeto que no hace nada
return new NullLogger();
}
}
}
class Program
{
static void Main(string[] args)
{
// Caso 1: Logging activado
ILogger logger1 = LoggerFactory.GetLogger(true);
logger1.Log("Iniciando sistema..."); // Imprime en consola
// Caso 2: Logging desactivado
ILogger logger2 = LoggerFactory.GetLogger(false);
// ¡NO necesitamos if (logger2 != null)!
// Llamamos al método con total confianza. No hará nada, pero no fallará.
logger2.Log("Esto no se verá, pero no explota.");
Console.WriteLine("Fin del programa.");
}
}Null Object vs C# Nullable Types (?)
Con las versiones modernas de C# (8.0+), tenemos Nullable Reference Types. El compilador nos avisa si intentamos acceder a un posible nulo.
ILogger? logger = GetLogger(); // El ? indica que puede ser nulo
logger?.Log("Hola"); // El operador ?. es seguro¿Significa esto que el patrón ha muerto? No.
- El operador
?.sigue siendo un “check” (unifoculto). - El patrón Null Object es comportamiento, no solo seguridad. A veces quieres que el “objeto vacío” tenga valores por defecto (ej: un
UsuarioInvitadocon nombre “Anónimo”), no solo que no explote.
El patrón Null Object es una cuestión de confianza. Permite que tus objetos confíen en sus colaboradores. Elimina el ruido visual de los chequeos defensivos y hace que el código fluya de manera lineal.
Casos de uso en el Mundo Real
- Colecciones vacías: si la ausencia y una colección sin elementos significan lo mismo en tu API, devolver
Enumerable.Empty<T>()evita una comprobación antes delforeach. - Tests Unitarios: A menudo usamos objetos nulos (o Mocks que no hacen nada) para aislar la clase que estamos testeando.
- Patrón Strategy: Si tienes una estrategia opcional, en lugar de poner
if (estrategia != null), pon unaDoNothingStrategy.