stringtranslate.com

Patrón de objeto nulo

En la programación orientada a objetos , un objeto nulo es un objeto sin valor referenciado o con un comportamiento neutro definido ( nulo ). El patrón de diseño de objetos nulos , que describe los usos de dichos objetos y su comportamiento (o falta de él), se publicó por primera vez como "Valor nulo" [1] y más tarde en la serie de libros Lenguajes de patrones de diseño de programas como "Objeto nulo". [2]

Motivación

En la mayoría de los lenguajes orientados a objetos, como Java o C# , las referencias pueden ser null . Es necesario comprobar estas referencias para asegurarse de que no sean null antes de invocar cualquier método , ya que, por lo general, los métodos no se pueden invocar en referencias null.

El lenguaje Objective-C adopta otro enfoque para este problema y no hace nada cuando se envía un mensaje a nil; si se espera un valor de retorno, nil(para objetos), se devuelve 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. [3]

Descripción

En lugar de utilizar una referencia nula para transmitir la ausencia de un objeto (por ejemplo, un cliente inexistente), se utiliza un objeto que implementa la interfaz esperada , pero cuyo cuerpo de método está vacío. Un propósito clave de utilizar un objeto nulo es evitar condicionales de diferentes tipos, lo que da como resultado un código más centrado y más rápido de leer y seguir, es decir, una legibilidad mejorada. Una ventaja de este enfoque sobre una implementación predeterminada funcional es que un objeto nulo es muy predecible y no tiene efectos secundarios: 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 de ellos. En el caso de una carpeta vacía, una respuesta puede 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 de hecho 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 de retorno sea, de hecho, una lista. La función que realiza la llamada puede simplemente iterar la lista de forma normal, sin hacer nada en realidad. Sin embargo, aún es posible verificar si el valor de retorno es un objeto nulo (una lista vacía) y reaccionar de manera diferente si se desea.

El patrón de objeto nulo también se puede utilizar para actuar como un código auxiliar para realizar pruebas, si una determinada característica, como una base de datos, no está disponible para realizar pruebas.

Ejemplo

Dado un árbol binario , con esta estructura de nodos:

nodo de clase { nodo izquierdo nodo derecho}

Se puede implementar un procedimiento de tamaño de árbol de forma recursiva:

función tamaño_árbol(nodo) { devuelve 1 + tamaño_árbol(nodo.izquierda) + tamaño_árbol(nodo.derecha)}

Dado que los nodos secundarios pueden no existir, se debe modificar el procedimiento agregando comprobaciones de inexistencia o nulas:

función tamaño_árbol(nodo) { conjunto suma = 1 Si node.left existe { suma = suma + tamaño_árbol(nodo.izquierda) } Si node.right existe { suma = suma + tamaño_árbol(nodo.derecha) } devolver suma}

Sin embargo, esto hace que el procedimiento sea más complicado al mezclar comprobaciones de límites con lógica normal, y se vuelve más difícil de leer. Al utilizar el patrón de objeto nulo, se puede crear una versión especial del procedimiento, pero solo para nodos nulos:

función tamaño_árbol(nodo) { devuelve 1 + tamaño_árbol(nodo.izquierda) + tamaño_árbol(nodo.derecha)}
función tamaño_árbol(nodo_nulo) { devuelve 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 como un caso especial del patrón Estado y del patrón Estrategia .

No es un patrón de Design Patterns , pero se menciona en Refactoring [4] de Martin Fowler y en Refactoring To Patterns [5] de Joshua Kerievsky como la refactorización Insertar objeto nulo .

El capítulo 17 del libro de Robert Cecil Martin Desarrollo de software ágil: principios, patrones y prácticas [6] está dedicado a este patrón.

Alternativas

Desde C# 6.0 es posible utilizar el operador "?." (también conocido como operador condicional nulo ), que simplemente evaluará como nulo si su operando izquierdo es nulo.

// compilar como aplicación de consola, requiere C# 6.0 o superior usando System ; namespace ConsoleApplication2 { class Program { static void Main ( string [] args ) { string str = "test" ; Console.WriteLine ( str ? .Longitud ) ; Console.ReadKey (); } } } // La salida será: // 4                  

Métodos de extensión y coalescencia nula

En algunos lenguajes de Microsoft .NET , los métodos de extensión se pueden utilizar para realizar lo que se denomina "fusión de valores 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", mientras que, de hecho, los métodos de extensión son estáticos. Se puede hacer que los métodos de extensión comprueben los valores nulos, lo que libera al código que los utiliza de tener que hacerlo. Tenga en cuenta que el siguiente ejemplo utiliza el operador de fusión de valores nulos de C# para garantizar una invocación sin errores, donde también podría haberse utilizado un if...then...else más mundano. El siguiente ejemplo solo funciona cuando no le importa la existencia de valores nulos o trata los valores nulos y las cadenas vacías de la misma manera. Es posible que la suposición no se cumpla en otras aplicaciones.

// compilar como aplicación de consola, requiere C# 3.0 o superior using System ; using System.Linq ; namespace MyExtensionWithExample { public static class StringExtensions { public static int SafeGetLength ( this string valueOrNull ) { return ( valueOrNull ?? string . Empty ). Length ; } } public static class Program { // definir algunas cadenas static readonly string [] strings = new [] { "Mr X." , "Katrien Duck" , null , "Q" }; // escribir la longitud total de todas las cadenas en la matriz public static void Main ( string [] args ) { var query = from text in strings select text . SafeGetLength (); // no es necesario realizar ninguna comprobación aquí Console . WriteLine ( query . Sum ()); } } } // La salida será: // 18                                                                

En varios idiomas

C++

Un lenguaje con referencias a objetos tipados estáticamente ilustra cómo el objeto nulo se convierte en un patrón más complicado:

#include <flujo de datos> clase Animal { público : virtual ~ Animal () = predeterminado ;        vacío virtual MakeSound () const = 0 ; };     clase Perro : público Animal { público : virtual void MakeSound () const override { std :: cout << "¡guau!" << std :: endl ; } };                  clase NullAnimal : público Animal { público : virtual void MakeSound () const override {} };            

Aquí, la idea es que hay situaciones en las que Animalse requiere un puntero o una referencia a un objeto, pero no hay ningún objeto apropiado disponible. Una referencia nula es imposible en C++ que cumple con los estándares. Un Animal*puntero nulo es posible y podría ser útil como marcador de posición, pero no se puede utilizar para el envío directo: a->MakeSound()es un comportamiento indefinido si aes un puntero nulo.

El patrón de objeto nulo resuelve este problema proporcionando una NullAnimalclase especial que puede instanciarse vinculada a un Animalpuntero o referencia.

La clase nula especial debe crearse para cada jerarquía de clases que vaya a tener un objeto nulo, ya que a 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.

Tenga en cuenta que NO tener una clase nula es una característica importante, a diferencia de los lenguajes donde "todo es una referencia" (por ejemplo, Java y C#). En C++, el diseño de una función o método puede indicar explícitamente si se permite o no el valor nulo.

// Función que requiere una instancia de |Animal| y no aceptará valores nulos. void DoSomething ( const Animal & animal ) { // |animal| nunca puede ser nulo aquí. }     // Función que puede aceptar una instancia de |Animal| o nula. void DoSomething ( const Animal * animal ) { // |animal| puede ser nulo. }     

DO#

C# es un lenguaje en el que se puede implementar correctamente el patrón de objeto nulo. Este ejemplo muestra objetos animales que muestran sonidos y una instancia de NullAnimal utilizada en lugar de la palabra clave null de C#. El objeto nulo proporciona un comportamiento coherente y evita una excepción de referencia nula en tiempo de ejecución que se produciría si se utilizara la palabra clave null de C# en su lugar.

/* Implementación del patrón de objeto nulo: */ using System ; // La interfaz Animal es la clave para la compatibilidad con las implementaciones de Animal 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 se debe usar 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 Perro : Animal { public override void MakeSound () { Console . WriteLine ( "¡Guau!" ); } }      /* ========================== * Ejemplo de uso simplista en un punto de entrada principal. */ static class Program { static void Main () { IAnimal dog = new Dog (); dog . MakeSound (); // genera "¡Guau!"         /* En lugar de utilizar C# null, utilice la instancia Animal.Null.  * Este ejemplo es simplista pero transmite la idea de que si se utiliza 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 utilizara C# null.  */ IAnimal unknown = Animal . Null ; //<< reemplaza: IAnimal unknown = null; unknown . MakeSound (); // no genera 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 es modelada por 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 sensible para su propósito puede devolverlo nilen su lugar, evitando así el caso especial de devolver "ningún objeto" que no es compatible con los diseñadores de Smalltalk. Este método tiene la ventaja de la simplicidad (no es necesario un caso especial) sobre el enfoque clásico de "nulo" o "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 tratar con posibles referencias a nilen programas de Smalltalk.

Ceceo común

En Lisp, las funciones pueden aceptar sin problemas 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 que resulta muy útil y da como resultado un código más corto.

Dado nil que en Lisp la lista está vacía, la situación descrita en la introducción anterior no existe. El código que retorna nildevuelve lo que en realidad es la lista vacía (y nada que se parezca a una referencia nula a un tipo de lista), por lo que el autor de la llamada no necesita probar el valor para ver si tiene o no una lista.

El patrón de objeto nulo también se admite en el procesamiento de valores múltiples. Si el programa intenta extraer un valor de una expresión que no devuelve ningún valor, el comportamiento es que nilse sustituye el objeto nulo. Por lo tanto, (list (values))devuelve (nil)(una lista de un 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.

CIERRE

En Common Lisp, el objeto niles la única instancia de la clase especial null. Esto significa que un método puede especializarse en 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 emite un sonido al ladrar: ¡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 )) ( formato t "guau!~%" ))      ;; permite que (make-sound nil) funcione a través de la especialización en la clase nula. ;; cuerpo vacío inocuo: nil no produce 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 en symbolo listaceptarán un nilargumento. Por supuesto, nulltodavía se puede definir una especialización que sea una coincidencia más específica para nil.

Esquema

A diferencia de Common Lisp y muchos dialectos de Lisp, el dialecto Scheme no tiene un valor nulo que funcione de esta manera; las funciones cary cdrno se pueden aplicar a una lista vacía; por lo tanto, el código de aplicación de Scheme tiene que usar las funciones de predicado empty?o pair?para evitar esta situación, incluso en situaciones en las que un Lisp muy similar no necesitaría distinguir los casos vacíos y no vacíos gracias al comportamiento de nil.

Rubí

En lenguajes tipados 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 fin          def get_animal ( animal = NilAnimal . new ) animal fin  get_animal ( Perro . nuevo ) . sonido => "ladrido" get_animal . sonido => nulo    

Los intentos de aplicar parches directamente a NilClass en lugar de proporcionar implementaciones explícitas producen más efectos secundarios inesperados que beneficios.

JavaScript

En lenguajes tipados como JavaScript , la herencia del lenguaje no es necesaria para proporcionar el comportamiento esperado.

clase Perro { sonido () { devolver 'ladrido' ; } }       clase NullAnimal { sonido () { devolver null ; } }       función getAnimal ( tipo ) { tipo de retorno === 'perro' ? new Perro () : new NullAnimal (); }            [ 'perro' , null ]. map (( animal ) => getAnimal ( animal ). sound ()); // Devuelve ["ladrido", null]   

Java

Interfaz pública Animal { void makeSound (); }    clase pública Perro implementa Animal { public void makeSound () { System.out.println ( " ¡ guau! " ) ; } }        clase pública NullAnimal implementa Animal { public void makeSound () { // silencio... } }         

Este código ilustra una variación del ejemplo de C++ anterior, utilizando el lenguaje Java. Al igual que con C++, se puede crear una instancia de una clase nula en situaciones en las que Animalse requiere una referencia a un objeto, pero no hay ningún objeto apropiado disponible. AnimalEs posible crear un objeto nulo ( Animal myAnimal = null;) y podría ser útil como marcador de posición, pero no se puede utilizar para llamar a un método. En este ejemplo, myAnimal.makeSound();se generará 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 al proporcionar una NullAnimalclase especial que se puede instanciar como un objeto de tipo Animal. Al igual que con C++ y lenguajes relacionados, esa clase nula especial se debe crear para cada jerarquía de clases que necesite un objeto nulo, ya que a NullAnimalno sirve de nada cuando lo que se necesita es un objeto nulo que no implemente la Animalinterfaz.

PHP

interfaz  Animal {  función pública  hacerSonido (); } clase  Perro  implementa  Animal {  función pública  hacerSonido () { echo "Guau... \n " ; } }     clase  Gato  implementa  Animal {  función pública  hacerSonido () { echo "Miauuu... \n " ; } }     clase  NullAnimal  implementa  Animal {  función pública  makeSound () { // silencio... } }    $animalType  =  'elefante' ;función  makeAnimalFromAnimalType ( cadena  $animalType ) :  Animal {  switch  ( $animalType )  {  caso  'perro' :  devuelve  nuevo  Perro ();  caso  'gato' :  devuelve  nuevo  Gato ();  predeterminado :  devuelve  nuevo  NullAnimal ();  } }makeAnimalFromAnimalType ( $animalType ) -> makeSound ();  // ..el animal nulo no emite ningún sonidofunción  animalHacerSonido ( Animal  $animal ) :  void {  $animal -> hacerSonido (); }foreach  ([  makeAnimalFromAnimalType ( 'perro' ),  makeAnimalFromAnimalType ( 'NullAnimal' ),  makeAnimalFromAnimalType ( 'gato' ),  ]  como  $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 que la clase concreta proporciona su objeto nulo correspondiente en un campo estático Empty. Este enfoque se utiliza con frecuencia en .NET Framework ( String.Empty, EventArgs.Empty, Guid.Empty, etc.).

Clase pública Animal Pública Compartida Solo lectura Vacía Como Animal = New AnimalEmpty ()            Sub MakeSound () público reemplazable Console.WriteLine ( "¡Guau!" ) Fin de sub Fin de clase       Amigo No Heredable Clase AnimalEmpty Hereda Animal      Anulaciones públicas Sub MakeSound () ' Fin de Sub Fin de clase       

Crítica

Este patrón debe utilizarse con cuidado, ya que puede hacer que aparezcan errores o fallos durante la ejecución normal del programa. [7]

Se debe tener cuidado de no implementar este patrón solo para evitar verificaciones nulas y hacer que el código sea más legible, ya que el código más difícil de leer puede simplemente moverse 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 de hecho 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 solo valor denominado null o nil. Además, existe la necesidad adicional de probar que ningún código en ninguna parte asigne null en lugar del objeto nulo, porque en la mayoría de los casos y lenguajes con tipado estático, esto no es un error del compilador si el objeto nulo es de un tipo de referencia, aunque ciertamente conduciría a errores en tiempo de ejecución en partes del código donde se usó el patrón para evitar verificaciones nulas. Además de eso, en la mayoría de los lenguajes y asumiendo 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), verificar el objeto nulo en lugar del valor nulo o nil introduce una sobrecarga, al igual que el patrón singleton probablemente en sí mismo al obtener la referencia singleton.

Véase también

Referencias

  1. ^ Kühne, Thomas (1996). "Valor vacío". Actas de la Primera Conferencia Internacional sobre Tecnología Orientada a Objetos, White Object-Oriented Nights 1996 (WOON'96), San Petersburgo, Rusia .
  2. ^ Woolf, Bobby (1998). "Objeto nulo". En Martin, Robert; Riehle, Dirk; Buschmann, Frank (eds.). Lenguajes de patrones de diseño de programas 3 . Addison-Wesley.
  3. ^ "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 .
  4. ^ Fowler, Martin (1999). Refactorización. Mejorar el diseño del código existente . Addison-Wesley. ISBN 0-201-48567-2.
  5. ^ Kerievsky, Joshua (2004). Refactorización a patrones . Addison-Wesley. ISBN 0-321-21335-1.
  6. ^ Martin, Robert (2002). Desarrollo ágil de software: principios, patrones y prácticas . Pearson Education. ISBN 0-13-597444-5.
  7. ^ Fowler, Martin (1999). Refactorización, pág. 216.

Enlaces externos