Special Case Pattern / Null Object Pattern

Descripción general

En todos los tratados de programación orientada a objetos se hace mención al problema que supone escribir funciones que devuelven objetos con valores null, ya que después de llamarlas y antes de poder usar el valor devuelto hay que comprobar si tienen el valor [null] para hacer una cosa u otra y evitar el ya comentado error [NullReferenceException].

[TOC] Tabla de Contenidos


↑↑↑

Special Case Pattern / Null Object Pattern


↑↑↑

Problema

En los lenguajes orientados a objetos es normal que un objeto instanciado tome el valor null. También es frecuente el error que se produce cuando se intenta usar uno de estos objetos, el error NullReferenceException.

Por otra parte, en todos los tratados de programación orientada a objetos se hace mención al problema que supone escribir funciones que devuelven objetos con valores null, ya que después de llamarlas y antes de poder usar el valor devuelto hay que comprobar si tienen el valor [null] para hacer una cosa u otra y evitar el ya comentado error [NullReferenceException].

La solución más evidente a este problema es que las funciones en lugar de devolver un valor [null] lancen una excepción. Esta forma de actuar simplifica el código, pero sigue obligándonos a comprobar la presencia de excepciones

Una solución más elegante es el uso de objetos especiales, que consiste en devolver un objeto especial que cumple con la interfaz esperada pero que sus métodos están vacios y no hace nada

Por ejemplo, Supongamos una función que explora los directorios y devuelve por cada directorio una lista con los ficheros que contiene, para realizar alguna acción (que no nos interesa describir) en cada uno de los ficheros.

Cuando aparece un directorio vacio (sin ficheros), ya sabemos que podemos optar por devolver el valor Null, devolver una excepción, o devolver un objeto especial, una lista (que es lo que se espera recibir) pero vacía.

La solución más correcta e s la tercera, Utilizar algún patrón como Null Object pattern o una versión más general (Special Case) propuesta por Martin Fowler.

La solución que propone este patrón es devolver un Objeto nulo. La ventaja de devolver un objeto nulo (es decir, una lista vacía) en lugar del valor Null es que no hay que verificar el valor de retorno porque de hecho es una lista

La función de llamada siempre recibe un objeto lista (que es lo que espera) y puede iterar la lista de forma normal, como recibe una lista no se genera ningún error, y como está vacía, pues no hace nada con la lista, por lo que el código trata ese caso especial de forma trasparente.

Sin embargo, es posible comprobar si el valor de retorno es un objeto nulo (por ejemplo, una lista vacía) y reaccionar de manera diferente si se desea.


↑↑↑

Discusión

La clave del patrón Null Object es una clase abstracta que define la interfaz para todos los objetos de este tipo. El patrón se implementa como una clase derivada de dicha clase abstracta. Por lo que puede ser usada en cualquier lugar que este tipo de objeto sea necesario.

La clase Objeto Null, tiene la misma interfaz que una clase "Real" y "sabe" que hacer en cada uno de sus métodos. La alternativa a usar este patrón, es usar el valor "null" que, evidentemente, no implementa la interfaz abstracta y que ya hemos comentado que debe comprobarse su valor por código específico en todos los objetos que empleemos.

Esquema del patrón

Client

AbstractObject

RealObject

NullObject


↑↑↑

Reglas prácticas


↑↑↑

Relación con otros patrones

Se puede considerar como un caso especial de State pattern and the Strategy pattern.

No es un patrón de patrones de diseño , pero se menciona en Martin Fowler Refactoring [2] y Josué Kerievsky de [3] libro sobre refactorización en el Insertar objeto nulo refactorización .


↑↑↑

Un ejemplo en C#


/* Null Object Pattern implementation:*/
using System; 
// Interfaz de los animales es la clave para la compatibilidad
// para las implementaciones de los animales inferiores.
interface IAnimal
{
    void Sonido();
}
 
// Perro es un animal real
class Perro : IAnimal
{
    public void Sonido()
    {
        Console.WriteLine("Guau!");
    }
}
 
// El caso nulo: esta clase NullAnimal deberá ser
    instanciada 
// y utilizarse en lugar de la palabra clave null.
class NullAnimal : IAnimal
{
    public void Sonido()
    {
        // no se especifica ( a proposito) 
        // ningún comportamiento..
    }
}
 
/* =========================
 * Ejemplo de uso simple de entrada principal.
 */
static class Program
{
    static void Main()
    {
        IAnimal dog = new Perro();
        dog.Sonido(); // Salida "Guau!"
 
       /* En lugar de utilizar C # Null, utilice una instancia
           NullAnimal.
 * Este ejemplo es simplista pero transmite la idea de
    que si 
* una instancia de NullAnimal se utiliza entonces el programa
 * Nunca va a experimentar una System.NullReferenceException.
    NET 
* en tiempo de ejecución.
 */
        IAnimal desconocido = new NullAnimal();  
        //<< Reemplaza: IAnimal unknown = null;
        desconocido.Sonido(); 
       // Ninguna salida, pero no se producirá una excepción
           en tiempo de ejecución 
    }
}


↑↑↑

Un ejemplo un poco más complejo

Supongamos una clase a la que llamaremos [AccesoDatosPAD] y su trabajo consiste en la puesta al día de un pequeño fichero de acceso aleatorio por índice. Para simplificar la tarea el fichero tiene una capacidad previamente predefinida (por ejemplo 50 registros)

Existe una clase [Registro] que contiene un registro que se grabara / leerá en el fichero este registro debe tener el tamaño fijo, por lo que los campos de texto se rellenan de blancos hasta alcanzar el tamaño que debe tener ese campo.

Como todos los registros tienen el mismo tamaño, para acceder al registro que ocupa, por ejemplo, la quinta posición, lo que tengo que hacer es situarme al principio del fichero, y después saltar el número de bytes que corresponden a cada registro multiplicado por uno menos que la posición de acceso (en este ejemplo por cuatro) y a continuación leer un registro.

El registro que se lee puede contener datos o estar vacio, la función que lee el registro será más o menos algo parecido a esto:


private IRegistro ReadRegistro(System.IO.BinaryReader br)
   {
 
       /* -----------------------------------------------------
 * He aplicado el patrón Caso Especial
 * Consiste en diseñar un objeto para un caso especial

* que se use mucho, en este caso un registro vacio
 * Esta función usa ese patrón y cuando lee el
    fichero 
* devuelve un objeto Registro o un Objeto RegistroEmpty
 * En realidad y en aplicación del patrón devuelve
    un objeto interface.
 * ---------------------------------------------------
 */

       // Instanciar un registro normal para leer los datos
       Registro nuevoRegistro = new Registro();

       // Proceso de lectura de los datos del registro en el fichero
       nuevoRegistro.Borrado = br.ReadBoolean();
       nuevoRegistro.Nombre  = br.ReadString();
       nuevoRegistro.Edad    = br.ReadInt32();
       nuevoRegistro.Numero  = br.ReadInt64();

       // Elegir el tipo de objeto que se devuelve, 
       // Uno especial o uno normal
       if (nuevoRegistro.IsEmpty == true)
       {
           return RegistroEmpty.Instance;
       }
       else
       {
           return nuevoRegistro;
       }
   }
   
   

↑↑↑

Otro Ejemplo copiado

Ejemplo del patrón de diseño No Operation

En un programa que emplea un lenguaje de programación orientado a objetos estos están constantemente relacionándose entre sí a través de llamadas a métodos y a través de las referencias que un objeto posee de otros. Sin embargo, es habitual que un determinado método devuelva un null en vez de una referencia a un objeto. Esta referencia null puede ser un problema ya que nos obliga en el código hacer una comprobación antes de poder llamarlo. Si un método devuelve un null puede dar como resultado un [NullReferenceException] en otra parte de la aplicación en donde se intente usar esa referencia y no se haga la comprobación.

Para tratar de evitar llenar nuestro código java de sentencias if con la comprobación de null podemos utilizar el patrón de diseño [Null Object Pattern]. La idea de este patrón es que en vez de devolver un null como resultado de la llamada a un método devolvamos un objeto que no haga nada en las llamadas a los métodos en los que se use. Por ejemplo, supongamos que tenemos un método que en base a un enum se encarga de devolver un objeto que sigue el patrón Command. Y ahora supongamos que para cierto valor del enum no hay objeto command que se pueda devolver, podríamos devolver null en cuyo caso nos veríamos obligados a realizar la comprobación por null o empleando la idea del patrón [Null Object Pattern] devolver un objeto que implemente la interfaz command en cuestión pero que no haga nada o haga una operación inocua. Si vemos que en un programa estamos llenándolo de sentencias if (objeto == null) tal vez podamos aplicar este patrón. Lo importante para poder eliminar esos if es determinar que es una operación inocua, si se trata de un objeto puede ser que el método no haga nada, si se trata de un número que se utiliza para sumar o multiplicar se puede devolver 0 o 1 respectivamente en vez de null, depende del caso y la operación a simular.

Esta patrón puede usarse también para evitar la excepción [NullReferenceException] pero no es tanto la misión del patrón la misión como evitar preocuparnos por si las referencias son null o no y eliminar ifs, es cierto que empleándolo no dará la excepción pero si la aplicación continua puede producir otra excepción o un comportamiento no deseado más complicado de resolver y de averiguar su causa en otra parte del código.

Veámoslo con el ejemplo de una factoría que para determinados enumerados se devuelve un objeto que sigue el patrón command pero para ciertos valores del enumerado no hay command válido y en vez de devolver null devolvemos un command [Null Object Pattern] este es el caso de llamar a la factoría con un enumerado null. Para el enumerado [Operacion. Mensaje] se devuelve un command que emite un mensaje, para [Operacion. No_Mensaje ] y [null] se devuelve un command que no hace nada.

    Module Module1

    Sub Main()
        Dim objTest As New TestOperacionCommandFactory
    End Sub

    Public Class OperacionCommandFactory

        Public Enum Operacion
            None
            Mensaje
            No_Mensaje
        End Enum

        Public Function buildCommand(operacion As Operacion) As OperacionCommand
            If operacion = Nothing Then
                Return New NoOperacionCommand()
            End If
            Select Case operacion
                Case OperacionCommandFactory.Operacion.Mensaje
                    Return New MensajeCommand()
                Case OperacionCommandFactory.Operacion.None
                    Return New NoOperacionCommand()
                Case OperacionCommandFactory.Operacion.No_Mensaje
                    Return New NoOperacionCommand()
                Case Else
                    Throw New System.ComponentModel.InvalidEnumArgumentException("Opcion no valida", operacion, GetType(Operacion))
            End Select
        End Function
    End Class


    Public Interface OperacionCommand
        Sub Operacion()
    End Interface

    Public Class NoOperacionCommand
        Implements OperacionCommand
        Public Sub Operacion() Implements OperacionCommand.Operacion
            Console.WriteLine("Esta linea no debe escribirse, solo lo hago para saber que funciona")
        End Sub
    End Class

    Public Class MensajeCommand
        Implements OperacionCommand
        Public Sub Operacion() Implements OperacionCommand.Operacion
            Console.WriteLine("Hola mundo!")
        End Sub
    End Class

    Public Class TestOperacionCommandFactory
        Private factory As OperacionCommandFactory = Nothing
        Public Sub New()
            Console.WriteLine("Empieza el test")
            factory = New OperacionCommandFactory()
            Call test1()
            Call test2()
            Call test3()
            Console.WriteLine("/Eof el test")
            Console.ReadKey()
        End Sub
        Public Sub test1()
            Dim operacion As OperacionCommand = factory.buildCommand(OperacionCommandFactory.Operacion.Mensaje)
            operacion.Operacion()
        End Sub
        Public Sub test2()
            Dim operacion As OperacionCommand = factory.buildCommand(OperacionCommandFactory.Operacion.No_Mensaje)
            operacion.Operacion()
        End Sub
        Public Sub test3()
            Dim operacion As OperacionCommand = factory.buildCommand(OperacionCommandFactory.Operacion.None)
            operacion.Operacion()
        End Sub
        Public Sub test4()
            Dim operacion As OperacionCommand = factory.buildCommand(Nothing)
            operacion.Operacion()
        End Sub
    End Class
End Module


↑↑↑

Bibliografía


↑↑↑

A.2.Enlaces

[Para saber mas]
[Grupo de documentos]
[Documento Index]
[Documento Start]
[Imprimir el Documento]