En la programación orientada a objetos , un objeto nulo es un objeto sin valor referenciado o con un comportamiento neutro ( nulo ) definido. El patrón de diseño de objeto nulo , que describe los usos de dichos objetos y su comportamiento (o la falta del mismo), se publicó por primera vez como "Valor Vacío" [ 1 ] y posteriormente en la serie de libros Pattern Languages of Program Design como "Objeto Nulo" [ 2 ] .
Motivación
En la mayoría de los lenguajes orientados a objetos, como Java o C# , las referencias pueden ser nulas . Es necesario comprobar que estas referencias no sean nulas antes de invocar cualquier método , ya que normalmente no se pueden invocar métodos sobre referencias nulas.
El lenguaje Objective-C adopta otro enfoque para este problema y no hace nada al enviar un mensaje a nil; si se espera un valor de retorno, nil(para objetos), 0 (para valores numéricos), NO(para BOOLvalores), o una estructura (para tipos de estructura) con todos sus miembros inicializados a null/0/ NO/estructura inicializada a cero se devuelve. [ 3 ]
Descripción
En lugar de usar una referencia nula para indicar la ausencia de un objeto (por ejemplo, un cliente inexistente), se usa un objeto que implementa la interfaz esperada , pero cuyo cuerpo del método está vacío. Un propósito clave de usar un objeto nulo es evitar condicionales de diferentes tipos, lo que resulta en un código más conciso, más fácil de leer y comprender, es decir, con una mayor legibilidad. Una ventaja de este enfoque sobre una implementación predeterminada funcional es que un objeto nulo es muy predecible y no tiene efectos secundarios: simplemente no hace nada .
Por ejemplo, una función puede recuperar una lista de archivos en una carpeta y realizar alguna acción en cada uno. En el caso de una carpeta vacía, una posible respuesta podría ser lanzar una excepción o devolver una referencia nula en lugar de una lista. Por lo tanto, el código que espera una lista debe verificar que efectivamente la tiene antes de continuar, lo que puede complicar el diseño.
Al devolver un objeto nulo (es decir, una lista vacía), no es necesario verificar que el valor devuelto sea realmente una lista. La función que realiza la llamada puede simplemente iterar sobre la lista como de costumbre, sin hacer prácticamente nada. Sin embargo, aún es posible comprobar si el valor devuelto es un objeto nulo (una lista vacía) y actuar de forma diferente si se desea.
El patrón de objeto nulo también puede utilizarse como un simulacro para realizar pruebas, si una determinada característica, como una base de datos, no está disponible para su prueba.
Ejemplo
Dado un árbol binario con la siguiente estructura de nodos:
clase pública ÁrbolBinario { privado ÁrbolBinario izquierda ; privado ÁrbolBinario derecha ;public BinaryTree ( BinaryTree left , BinaryTree right ) { this . left = left ; this . right = right ; }// getters, setters, métodos... }Se puede implementar un procedimiento para determinar el tamaño del árbol de forma recursiva:
clase pública ÁrbolBinario { // ...public int size ( BinaryTree bt ) { return 1 + size ( bt . getLeft ()) + size ( bt . getRight ()); } }Dado que los nodos hijos pueden no existir, es necesario modificar el procedimiento añadiendo comprobaciones de no existencia o de valores nulos:
clase pública ÁrbolBinario { // ...public int size ( BinaryTree bt ) { int sum = 1 ; if ( bt.getLeft ( ) != null ) { sum += size ( bt.getLeft ( ) ); } if ( bt.getRight ( ) ! = null ) { sum += size ( bt.getRight ( ) ) ; } return sum ; } }Sin embargo, esto complica el procedimiento al mezclar comprobaciones de límites con lógica normal, y resulta más difícil de leer. Utilizando el patrón de objeto nulo, se puede crear una versión especial del procedimiento, pero solo para nodos nulos:
clase pública ÁrbolBinario { // ...public int size ( BinaryTree bt ) { if ( bt == null ) { return 0 ; } else { return 1 + size ( bt . getLeft ()) + size ( bt . getRight ()); } }// Si se utiliza el operador ternario: public int size ( BinaryTree bt ) { return bt != null ? 1 + size ( bt . getLeft ()) + size ( bt . getRight ()) : 0 ; } }Esto separa la lógica normal del manejo de casos especiales y hace que el código sea más fácil de entender.
Relación con otros patrones
Puede considerarse un caso especial del patrón de Estado y del patrón de Estrategia .
No es un patrón de Design Patterns , pero se menciona en Refactoring de Martin Fowler [ 4 ] y Refactoring To Patterns de Joshua Kerievsky [ 5 ] como la refactorización Insert Null Object .
El capítulo 17 de Desarrollo ágil de software: principios, patrones y prácticas de Robert Cecil Martin [ 6 ] está dedicado al patrón.
Alternativas
A partir de C# 6.0 es posible utilizar el operador "?" ( también conocido como operador condicional nulo ), que simplemente se evaluará como nulo si su operando izquierdo es nulo.
espacio de nombres Wikipedia.Ejemplos ;// compilar como aplicación de consola, requiere C# 6.0 o superior usando System ;public class Program { static void Main ( string [] args ) { string str = "test" ; Console . WriteLine ( str ?. Length ); Console . ReadKey (); } } // La salida será: // 4Métodos de extensión y coalescencia nula
En algunos lenguajes .NET de Microsoft , los métodos de extensión se pueden usar para realizar lo que se denomina "consolidación de nulos". Esto se debe a que los métodos de extensión se pueden llamar en valores nulos como si se tratara de una "invocación de método de instancia", cuando en realidad son estáticos. Los métodos de extensión se pueden configurar para comprobar si hay valores nulos, liberando así al código que los usa de tener que hacerlo. Tenga en cuenta que el siguiente ejemplo usa el operador de consolidación de nulos de C# para garantizar una invocación sin errores, cuando también podría haber usado una estructura condicional más simple (if...then...else). El siguiente ejemplo solo funciona si no le importa la existencia de nulos o si trata los valores nulos y las cadenas vacías de la misma manera. Esta suposición puede no ser válida en otras aplicaciones.
espacio de nombres Wikipedia.Ejemplos ;// compilar como aplicación de consola, requiere C# 3.0 o superior using System ; using System.Linq ;clase estática StringExtensions { public static int SafeGetLength ( this string valueOrNull ) { return ( valueOrNull ?? String . Empty ). Length ; } }public static class Program { // define algunas cadenas static const string [] MY_STRINGS = new [] { "Sr. X." , "Katrien Duck" , null , "Q" };// escribe la longitud total de todas las cadenas en el array public static void Main ( string [] args ) { IEnumerable < int > query = from text in MY_STRINGS select text . SafeGetLength (); // no es necesario hacer ninguna comprobación aquí Console . WriteLine ( query . Sum ()); } } // La salida será: // 18En varios idiomas
C++
Un lenguaje con referencias a objetos tipadas estáticamente ilustra cómo el objeto nulo se convierte en un patrón más complejo:
importar std ;usando std :: unique_ptr ;clase Animal { público : virtual ~ Animal () = predeterminado ;virtual void makeSound () const = 0 ; };clase Perro : público Animal { público : virtual void makeSound () const override { std :: println ( "¡Guau!" ); } };clase NullAnimal : público Animal { público : virtual void makeSound () const override { // silencio... } };int main () { unique_ptr < Animal > perro = std :: make_unique < Perro > (); perro -> makeSound (); // emite "¡Guau!"unique_ptr < Animal > unknown = std :: make_unique < NullAnimal > (); unknown -> makeSound (); // no produce ninguna salida, pero no lanza una excepción en tiempo de ejecución }Aquí, la idea es que existen situaciones en las que Animalse requiere un puntero o referencia a un objeto, pero no hay ningún objeto apropiado disponible. Una referencia nula es imposible en C++ estándar. Un Animal*puntero nulo es posible y podría ser útil como marcador de posición, pero no se puede usar para despacho directo: a->makeSound()el comportamiento es indefinido si aes un puntero nulo.
El patrón de objeto nulo resuelve este problema al proporcionar una NullAnimalclase especial que puede instanciarse vinculada a un Animalpuntero o referencia.
Se debe crear una clase nula especial para cada jerarquía de clases que vaya a tener un objeto nulo, ya que NullAnimalno sirve de nada cuando lo que se necesita es un objeto nulo con respecto a alguna Widgetclase base que no está relacionada con la Animaljerarquía.
Cabe destacar que la ausencia total de una clase nula es una característica importante, a diferencia de lenguajes donde "todo es una referencia" (por ejemplo, Java y C#). En C++, el diseño de una función o método puede especificar explícitamente si se permite o no el uso de valores nulos.
// Función que requiere una instancia de Animal y no acepta valores nulos. void doSomething ( const Animal & animal ) { // animal nunca puede ser nulo aquí. }// Función que puede aceptar una instancia de Animal o null. void doSomething ( const Animal * animal ) { // animal puede ser null. }DO#
C# es un lenguaje en el que el patrón de objeto nulo se puede implementar correctamente. Este ejemplo muestra objetos animales que emiten sonidos y una instancia de NullAnimal utilizada en lugar de la palabra clave null de C#. El objeto nulo proporciona un comportamiento consistente y evita una excepción de referencia nula en tiempo de ejecución que ocurriría si se utilizara la palabra clave null de C#.
espacio de nombres Wikipedia.Ejemplos ;// Implementación del patrón de objeto nulo: using System ;// La interfaz Animal es la clave de compatibilidad para las implementaciones de Animal que se muestran a continuación. interface IAnimal { void MakeSound (); }// Animal es el caso base. abstract class Animal : IAnimal { // Una instancia compartida que se puede usar para comparaciones public static readonly IAnimal Null = new NullAnimal (); // El caso nulo: esta clase NullAnimal debe usarse en lugar de la palabra clave null de C#. private class NullAnimal : Animal { public override void MakeSound () { // No proporciona ningún comportamiento a propósito. } } public abstract void MakeSound (); }// El perro es un animal real. class Dog : Animal { public override void MakeSound () { Console . WriteLine ( "¡Guau!" ); } }/* ========================= * Ejemplo de uso simple en un punto de entrada principal. */ static class Program { static void Main () { IAnimal dog = new Dog (); dog . MakeSound (); // emite "¡Guau!"/* En lugar de usar null de C#, use la instancia Animal.Null. * Este ejemplo es simple, pero transmite la idea de que si se usa la instancia Animal.Null, el programa * nunca experimentará una excepción System.NullReferenceException de .NET en tiempo de ejecución, a diferencia de si se usara null de C#. * / IAnimal unknown = Animal.Null ; // << reemplaza: IAnimal unknown = null; unknown.MakeSound (); // no muestra nada, pero no lanza una excepción en tiempo de ejecución } }Charla informal
Siguiendo el principio de Smalltalk, todo es un objeto ; la ausencia de un objeto se modela a su vez mediante un objeto llamado nil. En GNU Smalltalk, por ejemplo, la clase de niles UndefinedObject, un descendiente directo de Object.
Cualquier operación que no devuelva un objeto adecuado para su propósito puede devolver nilen su lugar, evitando así el caso especial de devolver "ningún objeto", no compatible con los diseñadores de Smalltalk. Este método tiene la ventaja de la simplicidad (no requiere un caso especial) frente al enfoque clásico de "nulo", "ningún objeto" o "referencia nula". Los mensajes especialmente útiles para usar con nilson isNil, ifNil:o ifNotNil:, que hacen que sea práctico y seguro manejar posibles referencias a nilen programas Smalltalk.
Lisp común
En Lisp, las funciones pueden aceptar elegantemente el objeto especial nil, lo que reduce la cantidad de pruebas de casos especiales en el código de la aplicación. Por ejemplo, aunque niles un átomo y no tiene ningún campo, las funciones cary cdrlo aceptan nily simplemente lo devuelven, lo cual es muy útil y resulta en un código más corto.
Dado que niles la lista vacía en Lisp, la situación descrita en la introducción anterior no existe. El código que devuelve nilestá devolviendo lo que es de hecho la lista vacía (y no algo que se parezca a una referencia nula a un tipo de lista), por lo que quien llama no necesita comprobar el valor para ver si tiene o no una lista.
El patrón de objeto nulo también es compatible con el procesamiento de múltiples valores. Si el programa intenta extraer un valor de una expresión que no devuelve ningún valor, nilse sustituye por el objeto nulo. Por lo tanto, (list (values))devuelve (nil)(una lista de un solo elemento que contiene nil). La (values)expresión no devuelve ningún valor, pero como la llamada a la función listnecesita reducir su expresión de argumento a un valor, el objeto nulo se sustituye automáticamente.
CLOS
En Common Lisp, el objeto niles la única instancia de la clase especial null. Esto significa que un método puede especializarse para la nullclase, implementando así el patrón de diseño nulo. Es decir, está esencialmente integrado en el sistema de objetos.
;; clase de perro vacía( defclass perro () ());; un objeto perro hace un sonido ladrando: ¡guau! se imprime en la salida estándar ;; cuando se llama a (make-sound x), si x es una instancia de la clase perro.( defmethod make-sound (( obj perro )) ( format t "¡guau!~%" ));; permite que (make-sound nil) funcione mediante especialización a la clase nula. ;; cuerpo vacío inocuo: nil no hace ningún sonido. ( defmethod make-sound (( obj null )))La clase nulles una subclase de la symbolclase, porque niles un símbolo. Dado que niltambién representa la lista vacía, también nulles una subclase de la listclase. Los parámetros de los métodos especializados para symbolo listtomarán, por lo tanto, un argumento. Por supuesto, aún se puede definir niluna especialización que sea una coincidencia más específica para .nullnil
Esquema
A diferencia de Common Lisp y muchos dialectos de Lisp, el dialecto Scheme no tiene un valor nil que funcione de esta manera; las funciones cary cdrno se pueden aplicar a una lista vacía; por lo tanto, el código de la aplicación Scheme tiene que usar las funciones de predicado empty?or pair?para evitar esta situación, incluso en situaciones en las que Lisp muy similar no necesitaría distinguir los casos vacíos y no vacíos gracias al comportamiento de nil.
Rubí
En lenguajes de tipado dinámico como Ruby , la herencia del lenguaje no es necesaria para proporcionar el comportamiento esperado.
clase Perro def sonido "ladrido" fin fin clase NilAnimal def sonido ( * ); fin findef obtener_animal ( animal = NilAnimal . nuevo ) animal finget_animal ( Perro . nuevo ) . sonido => "ladrido" get_animal . sonido => nilLos intentos de modificar directamente NilClass mediante parches en lugar de proporcionar implementaciones explícitas generan más efectos secundarios inesperados que beneficios.
JavaScript
En lenguajes con tipado dinámico como JavaScript , la herencia del lenguaje no es necesaria para proporcionar el comportamiento esperado.
clase Perro { sonido () { return 'ladrido' ; } }clase NullAnimal { sonido () { devolver null ; } }función obtenerAnimal ( tipo ) { return tipo === 'perro' ? nuevo Perro () : nuevo AnimalNulo (); }[ 'perro' , null ]. map (( animal ) => getAnimal ( animal ). sound ()); // Devuelve ["ladrido", null]Java
paquete org.wikipedia.examples ;interfaz Animal { void hacerSonido (); }class Dog implements Animal { public void makeSound () { System . out . println ( "¡Guau!" ); } }clase NullAnimal implementa Animal { public void makeSound () { // silencio... } }public class Example { public static void main ( String [] args ) { Animal dog = new Dog (); dog . makeSound (); // emite "¡Guau!"Animal desconocido = nuevo NullAnimal (); desconocido.makeSound ( ); // no produce ninguna salida, pero no lanza una excepción en tiempo de ejecución } }Este código ilustra una variación del ejemplo de C++ anterior, utilizando el lenguaje Java. Al igual que en C++, se puede instanciar una clase nula en situaciones donde Animalse requiere una referencia a un objeto, pero no hay ningún objeto apropiado disponible. Un Animalobjeto nulo es posible Animal myAnimal = null;y podría ser útil como marcador de posición, pero no se puede usar para llamar a un método. En este ejemplo, myAnimal.makeSound();se lanzará una NullPointerException. Por lo tanto, puede ser necesario código adicional para comprobar si hay objetos nulos.
El patrón de objeto nulo resuelve este problema proporcionando una NullAnimalclase especial que puede instanciarse como un objeto de tipo Animal. Al igual que en C++ y lenguajes relacionados, esa clase nula especial debe crearse para cada jerarquía de clases que necesite un objeto nulo, ya que un NullAnimalno es útil cuando lo que se necesita es un objeto nulo que no implementa la Animalinterfaz.
PHP
interfaz Animal { función pública hacerSonido (); }clase Perro implementa Animal { función pública hacerSonido () { echo "Guau... \n " ; } }clase Cat implementa Animal { función pública makeSound () { echo "Miau... \n " ; } }clase NullAnimal implementa Animal { función pública makeSound () { // silencio... } }$animalType = 'elephant' ;function makeAnimalFromAnimalType ( string $animalType ) : Animal { switch ( $animalType ) { case 'dog' : return new Dog (); case 'cat' : return new Cat (); default : return new NullAnimal (); } }makeAnimalFromAnimalType ( $animalType ) -> makeSound (); // ...el animal nulo no hace ningún sonidofunción animalMakeSound ( Animal $animal ) : void { $animal -> makeSound (); }foreach ([ makeAnimalFromAnimalType ( 'dog' ), makeAnimalFromAnimalType ( 'NullAnimal' ), makeAnimalFromAnimalType ( 'cat' ), ] as $animal ) { // Eso también reduce el código de manejo de valores nulos animalMakeSound ( $animal ); }Visual Basic .NET
La siguiente implementación del patrón de objeto nulo demuestra cómo la clase concreta proporciona su objeto nulo correspondiente en un campo estático Empty. Este enfoque se utiliza con frecuencia en el .NET Framework ( String.Empty, EventArgs.Empty, Guid.Empty, etc.).
Clase pública Animal Pública compartida Solo lectura Vacía como Animal = Nuevo AnimalEmpty ()Sub pública sobrescribible MakeSound () Console.WriteLine ( " ¡ Guau!" ) End Sub End ClassAmigo NoHeredable Clase AnimalVacío Hereda AnimalSub MakeSound () público anula la subclase MakeSound () ' Fin de la subclase Fin de la claseCrítica
Este patrón debe usarse con cuidado, ya que puede hacer que los errores/fallos parezcan una ejecución normal del programa. [ 7 ]
Se debe tener cuidado de no implementar este patrón solo para evitar comprobaciones de nulidad y hacer el código más legible, ya que el código más difícil de leer podría simplemente trasladarse a otro lugar y ser menos estándar, como cuando se debe ejecutar una lógica diferente en caso de que el objeto proporcionado sea realmente el objeto nulo. El patrón común en la mayoría de los lenguajes con tipos de referencia es comparar una referencia con un único valor denominado nulo o nil. Además, existe la necesidad adicional de comprobar que ningún código asigne nunca nulo en lugar del objeto nulo, porque en la mayoría de los casos y lenguajes con tipado estático, esto no es un error de compilación si el objeto nulo es de un tipo de referencia, aunque ciertamente provocaría errores en tiempo de ejecución en partes del código donde se usó el patrón para evitar comprobaciones de nulidad. Además, en la mayoría de los lenguajes y suponiendo que puede haber muchos objetos nulos (es decir, el objeto nulo es un tipo de referencia pero no implementa el patrón singleton de una u otra manera), comprobar el objeto nulo en lugar del valor nulo o nil introduce una sobrecarga, al igual que probablemente el propio patrón singleton al obtener la referencia singleton.
Véase también
Referencias
- ↑ Kühne, Thomas (1996). "Void Value". Actas de la Primera Conferencia Internacional sobre Tecnología Orientada a Objetos, White Object-Oriented Nights 1996 (WOON'96), San Petersburgo, Rusia .
- ↑ Woolf, Bobby (1998). "Objeto nulo". En Martin, Robert; Riehle, Dirk; Buschmann, Frank (eds.). Pattern Languages of Program Design 3 . Addison-Wesley.
- ↑ "Trabajar con objetos (Trabajar con nil)" . Biblioteca para desarrolladores de iOS . Apple, Inc. 13 de diciembre de 2012. Consultado el 19 de mayo de 2014 .
- ↑ Fowler, Martin (1999). Refactoring. Improving the Design of Existing Code . Addison-Wesley. ISBN 0-201-48567-2.
- ↑ Kerievsky, Joshua (2004). Refactoring To Patterns . Addison-Wesley. ISBN 0-321-21335-1.
- ↑ Martin, Robert (2002). Desarrollo ágil de software: principios, patrones y prácticas . Pearson Education. ISBN 0-13-597444-5.
- ↑ Fowler, Martin (1999). Refactoring, p. 216.
Enlaces externos
- El relato de Jeffery Walker sobre el patrón de objeto nulo.
- Descripción de Martin Fowler del caso especial, un patrón ligeramente más general.
- El patrón de objeto nulo revisitado
- Introducir la refactorización de objetos nulos
- Tutorial de SourceMaker
- Patrón de objeto nulo en Swift
- patrones de diseño de software
