Todos nosotros hemos estado usando mucho null.
Es cómodo, es eficiente, es rápido y, sin embargo, hemos sufrido un montón de problemas relacionados con su uso.
¿Cuál es el sesgo cognitivo que actualmente nos impide, además de reconocer el problema, empezar a solucionarlo?
Nulo es una bandera. Representa diferentes situaciones según el contexto en el que se utilice e invoque.
Esto arroja el error más grave en el desarrollo de software: acoplar una decisión oculta en el contrato entre un objeto y quien lo usa .
Por si esto fuera poco, rompe la biyección que era nuestra única regla de diseño . Representando múltiples elementos del dominio con la misma entidad y obligando a tener interpretaciones contextuales .
Un buen principio de software nos desafía a tener una alta cohesión. Todos
los objetos deben ser lo más específicos posible y tener una sola responsabilidad (La S de Sólido ).
El objeto menos cohesivo de cualquier sistema es nuestro comodín: nulo
Nulo se asigna a varios conceptos diferentes en el mundo real
Null no es polimórfico con ningún objeto, por lo que cualquier función que lo invoque romperá la cadena de llamadas posteriores.
Ejemplo 1: Modelemos la interacción entre personas durante la pandemia actual de covid-19.
final class City { public function interactionBetween ($somePerson, $anotherPerson) { if ( $this ->meetingProbability() < random()) { return null ; //no interaction } else { return new PersonToPersonInteraction($somePerson, $anotherPerson); } } } final class PersonToPersonInteraction { public function propagate ($aVirus) { if ( $this ->somePerson->isInfectedWith($aVirus) && $aVirus->infectionProbability() > random()) { $this ->anotherPerson->getInfectedWith($aVirus); } } } $covid19 = new Virus(); $janeDoe = new Person(); $johnSmith = new Person(); $wuhan = new City(); $interaction = $wuhan->interactionBetween($johnSmith, $janeDoe); if ($interaction != null ) { $interaction->propagate($covid19); } /* In this example we modeled the interaction between an infected person and a healthy one. Jane is healthy but might be infected if Virus R0 applies to her.*/
Podemos ver que hay dos banderas nulas y las cláusulas if correspondientes.
La propagación nula parece estar contenida, pero las apariencias engañan.
La creación de null ocurrió debido a un evento fortuito en 1965.
Tony Hoare : El creador del algoritmo QuickSort y también ganador del Premio Turing (el Premio Nobel de Computación) lo agregó al lenguaje Algol porque parecía práctico y fácil de hacer. Varias décadas después mostró su arrepentimiento:
Este excelente artículo cuenta la historia en detalle:
Yo lo llamo mi error de mil millones de dólares... En ese momento, estaba diseñando el primer sistema completo de tipos para referencias en un lenguaje orientado a objetos. Mi objetivo era garantizar que todos los usos de las referencias fueran absolutamente seguros, con la verificación realizada automáticamente por el compilador.
Pero no pude resistir la tentación de poner una referencia nula, simplemente porque era muy fácil de implementar. Esto ha llevado a innumerables errores, vulnerabilidades y bloqueos del sistema, que probablemente han causado mil millones de dólares en dolor y daños en los últimos cuarenta años.
– Tony Hoare, inventor de ALGOL W.
El video completo también está disponible aquí .
Nosotros, como desarrolladores, usamos nulo porque es fácil (de escribir) y porque creemos que mejora la eficiencia de nuestro software.
Al cometer este error ignoramos que el código se lee hasta 10 veces más de lo que se escribe .
Leer código con nulos es más arduo y difícil. Por lo tanto, solo posponemos el problema más adelante.
Respecto a la eficiencia (que es la excusa más utilizada para generar acoplamiento ).
Salvo en casos muy concretos y críticos, su pérdida de rendimiento es despreciable. Y solo se justifica en aquellos sistemas que priorizan la eficiencia sobre la legibilidad, la adaptabilidad y la mantenibilidad (siempre hay una compensación con respecto a los atributos de calidad).
Este sesgo cognitivo persistió en el tiempo aunque, según el actual
Las máquinas virtuales modernas y de última generación optimizan el código para nosotros.
Para usar evidencia en lugar de intuición, solo necesitamos comenzar a comparar en su lugar
de seguir afirmando erróneamente que la eficiencia es más importante que
legibilidad.
Null se (ab)utiliza para enmascarar situaciones inesperadas y propagar el error en el código demasiado lejos, generando el tan temido efecto dominó.
Uno de los principios del buen diseño es fallar rápido.
Ejemplo 2: Dado un formulario de entrada de datos para un paciente, se nos solicita que completemos la fecha de nacimiento.
Si hay un error en el componente visual y la creación del objeto, podría construirse con una fecha de nacimiento nula .
Al ejecutar un proceso por lotes nocturno que recopila todas las fechas de la
pacientes para calcular una edad promedio, el paciente ingresado generará un error.
La pila con información útil para el desarrollador estará muy lejos de donde está presente el defecto. ¡Feliz depuración!
Photo by
Victoria Heath
on
Unsplash
Además, pueden existir diferentes sistemas con diferentes lenguajes de programación, transmisión de datos a través de una API, archivos, etc.
La peor pesadilla del desarrollador es tener que depurar ese error temprano en la mañana y tratar de encontrar la causa raíz del problema.
Null se usa de muchas maneras, como mencionamos anteriormente. Si permitimos modelos incompletos, la falta de integridad generalmente se modela con un valor nulo . Esto agrega complejidad al completar el código de controles con ifs.
La presencia de nulos genera código repetitivo y desordena el código con múltiples controles ifs .
Fomentar modelos incompletos nos obliga a cometer dos errores adicionales:
1. Contaminar código con setters para completar la información esencial necesaria.
Modelos Desnudos - Parte I: Setters
2. Construir modelos mutables violando la biyección ignorando que las entidades del mundo real no mutan en su esencia.
¿Es muy claro para todos que una fecha no debe mutar?
La mayoría de los lenguajes escritos evitan errores al garantizar que el objeto que se envía como parámetro (o se devuelve) puede responder a un determinado protocolo.
Desafortunadamente, algunos de estos lenguajes han dado un paso atrás de
permitiendo declarar que el objeto es de cierto tipo y (opcionalmente) nulo .
Esto rompe la cadena de invocaciones obligando a Ifs a controlar la ausencia del objeto violando el principio Sólido abierto/cerrado .
Además, los controles de tipo nulo corrompen. Si usamos un lenguaje escrito y confiamos en la red de defensa del compilador. Null logra penetrarlo como un virus y propagarse a los otros tipos como se indica a continuación.
No lo uses.
Como siempre, para resolver todos nuestros problemas debemos permanecer fieles a la única regla de diseño axiomática que nos hemos impuesto.
Busque soluciones en el dominio del problema para traerlas a nuestro modelo.
En el caso anterior, cuando los objetos deben declarar un tipo, existen soluciones más elegantes que evitan que los ifs modelen opcionalmente.
En los lenguajes de clasificación, basta con utilizar el patrón de diseño NullObject en nuestra clase hermana concreta y declarar el supertipo como un tipo del colaborador basado en el principio de sustitución de Liskov (L de SOLID).
Sin embargo, si decidimos implementar esa solución, estaremos violando otro principio de diseño que establece:
Deberíamos subclasificar por razones esenciales y no para reutilizar código o ajustar jerarquías de clases.
La mejor solución en un lenguaje de clasificación es declarar una interfaz a la que deben adherirse tanto la clase real como la clase de objeto nulo .
En el primer ejemplo:
Interface SocialInteraction { public function propagate ($aVirus) ; } final class SocialDistancing implements SocialInteraction { public function propagate ($aVirus) { //Do nothing !!!! } } final class PersonToPersonInteraction implements SocialInteraction { public function propagate ($aVirus) { if ( $this ->somePerson->isInfectedWith($aVirus) && $aVirus->infectionProbability() > random()) { $this ->anotherPerson->getInfectedWith($aVirus); } } } final class City { public function interactionBetween ($aPerson, $anotherPerson) { return new SocialDistancing(); // The cities are smart enough to implement social distancing to model Person to Person interactions } } $covid19 = new Virus(); $janeDoe = new Person(); $johnSmith = new Person(); $wuhan = new City(); $interaction = $wuhan->interactionBetween($johnSmith, $janeDoe); $interaction->propagate($covid19); /* Jane will not be affected since the interaction prevents from propagating the virus
¡No hay virus involucrados ni ifs ni nulls!
En este ejemplo, reemplazamos nulo con una especialización que, a diferencia de ella, existe en el dominio del problema.
Volvamos al ejemplo del formulario del paciente. Necesitábamos calcular el promedio dejando fuera los formularios no llenados.
Interface Visitable { public function accept ($aVisitor) ; } final class Date implements Visitable { public function accept ($aVisitor) { $aVisitor->visitDate( $this ); } } final class DateNotPresent implements Visitable { public function accept ($aVisitor) { $aVisitor->visitDateNotPresent( $this ); } } final class AverageCalculator { private $count = 0 ; private $ageSum = 0 ; public function visitDate ($aDate) { $this ->count++; $this ->ageSum += today() - $aDate; } public function visitDateNotPresent ($aDate) { } public function average () { if ( $this ->count == 0 ) return 0 ; else return $this ->ageSum / $this ->count; } } function averagePatientsAge ($patients) { $calculator = new AverageCalculator(); foreach ($patients as $patient) $patient->birthDate()->accept($calculator); return $calculator->average(); }
Usamos el patrón Visitor para navegar por objetos que pueden comportarse como objetos nulos.
sin valores nulos
Además, eliminamos lo no esencial si usamos polimorfismo y dejamos la solución abierta a otros cálculos además del promedio a través del principio abierto/cerrado.
Construimos una solución menos algorítmica pero más declarativa, mantenible y extensible.
Algunos idiomas admiten opcionalmente con el concepto de Maybe/Optional que es un caso particular de la solución propuesta implementada anteriormente a nivel de idioma.
El uso de null es una práctica desaconsejada basada en prácticas profundamente arraigadas en nuestro
industria. A pesar de esto, casi todos los lenguajes comerciales lo permiten y los desarrolladores lo utilizan.
Deberíamos, al menos, empezar a cuestionar su uso y ser más maduros y responsables en el desarrollo de software.
Parte del objetivo de esta serie de artículos es generar espacios de debate y discusión sobre el diseño de software.
Esperamos comentarios y sugerencias sobre este artículo.
Este artículo también está disponible en español aquí y en chino aquí .