sábado, 13 de mayo de 2017

NULL debe morir

Uno de los grandes errores de la programación orientada a objetos (OOP) con Java ha sido la existencia de las referencias a NULL.
Esta es la causa del popular y formidable error “java.lang.NullpointerException”. Toda una embrarrada.
La existencia de este tipo de referencia es la causa de muchos dolores de cabeza, de muchísimo código innecesario y defensivo, de muchas malas prácticas y de una pésima filosofía de desarrollo.




Las referencias a NULL nunca debieron existir, porque su abuso provoca código como el siguiente:

ProductHE product = deal.getProductHM();
PriceHEValue priceHE = null;
If (product ¡= null && product.getMortageRate() ¡= null && product.getMortageRate().getTaxRange() != null && product.getMortageRate().getTaxRange().getListPrices() != null && !product.getMortageRate().getTaxRange().getListPrices().isEmpty() && product.getMortageRate().getTaxRange().getListPrices().get(0) != null){
    priceHE = product.getMortageRate().getTaxRange().getListPrices().get(0);
}
deal.getContratHE().setPrimaryProductPrice(priceHE);

No sé por dónde empezar a decir TODO lo que está mal, que digo mal PÉSIMO, en este código.
Y lo más triste, este código está basado en hechos reales, código real, algunos nombres fueron cambiados para proteger la identidad de los involucrados!! ;)

¿Pueden creer que alguien se sintió inteligente al escribir esa basura?
Aquí no estamos para ser políticamente correctos. Eso es basura duélale a quien le duela.

Como dijo Hannibal Letcter, vamos por partes:


ProductHE product = deal.getProductHM();

¿Por qué demonios getProductHM() puede retornar un NULL?

Desde allí empiezan nuestros problemas. Eso implica que no podemos confiar en el método. Que no podemos confiar en el objeto, que no podemos confiar en sus propiedades….
Debemos ser defensivos al extremo, cuidadosos al trabajar con esos objetos miserables y estúpidos.

¿No me cree? ¿Le parece que exagero? Mire lo que viene a continuación entonces:

If (product ¡= null && product.getMortageRate() ¡= null && product.getMortageRate().getTaxRange() != null && product.getMortageRate().getTaxRange().getListPrices() != null && !product.getMortageRate().getTaxRange().getListPrices().isEmpty() && product.getMortageRate().getTaxRange().getListPrices().get(0) != null)

TODO ese código tiene el único propósito de evitar una NullpointerException; así de simple.
No tiene otra sentido ni fundamento.
Imagine un sistema enorme, en el que los módulos estén plagados de código por ese estilo. Esto ocurre porque no podemos confiar en nuestros objetos. Y eso es triste y terrible.

Pero las cosas no terminan allí, no señor.
Luego de todo ese dolor del orto, digo de cabeza, para evitar una NullpointerException resulta que procedemos a continuar con el programa asignando posibles referencias a NULL. Es decir, no aprendimos la lección.

deal.getContratHE().setPrimaryProductPrice(priceHE);

A estas alturas priceHE aún puede ser NULL y la asignamos y usamos así felizmente ignorantes.
Este código es tan malo, que ni siquiera tienen la decencia de trabajar los NULL con elegancia.

Voy a poner un ejemplo en el que, aunque se usa NULL y eso sigue siendo un error, al menos lo hacen de un modo que no perjudica la legibilidad del código. Imagine que dentro de la clase ProductHE, escribimos el siguiente método:

public final class ProductHE implements HEableProduct{
    public PriceHEValue getPrimaryPrice(){
        if (product.getMortageRate() == null) return null;
        if (product.getMortageRate().getTaxRange() == null) return null;
        if (product.getMortageRate().getTaxRange().getListPrices()  == null) return null;
        return product.getMortageRate().getTaxRange().getListPrices().get(0);
    }
}

El horroroso código anterior quedaría de la siguiente forma:

ProductHE product = deal.getProductHM();
deal.getContratHE().setPrimaryProductPrice(product == null? null: product. getPrimaryPrice());

¡Elegante! ¿No les parece?

Vamos, admítalo. Esta última versión, aunque usa NULL, es mucho más fácil de leer, mantener e incluso usar.

Todo el problema del código anterior, además de usar NULL, deriva de la desconfianza y del ego. Así es.

Desconfianza en el objeto idiota que nos puede lanzar una horrible e indeseable NullpointerException, y con razón. Ego porque al considerar al objeto anémico y estúpido, en vez de dejar que el objeto, product en este caso, trabaje para nosotros y haga el esfuerzo y tareas para las que debería haber sido diseñado, nosotros en nuestra “grandeza” optamos por pedirle un montón de datos al objeto para hacer todo el trabajo, cálculos y validaciones por nuestra cuenta, lo cual es una muy mala práctica en la OOP en general.
Esto rompe las reglas pigbar de programación.

Volviendo al tema central de esta publicación, sí no debemos usar NULL en todo nuestro código

¿Qué opciones tenemos?


-- 100 dólares y se los digo. Ok, Ok, sigamos sin cobrar…

Como siempre, tenemos un grupo de opciones que podemos usar.
En este caso vamos a ver dos de ellas. Una es el principio de Fail Fast – Fail a Lot, lo cual quiere decir que lancemos excepciones tan pronto y tan frecuentemente como lo necesitemos.

La otra es usar los NULL-Objects, que no es más que una implementación de los objetos diseñada para fallar cuando se intenten hacer cosas para las que no tiene permiso o simplemente no puede hacerlas, representa un objeto no válido del todo.

Nuestro ejemplo anterior podría ahora lucir de esta forma:

public final class ProductHE implements HEableProduct{
    public PriceHEValue getPrimaryPrice(){
       try{        
           return product.getMortageRate().getTaxRange().getListPrices().get(0);
        } catch(NoValidObjectException e){
            return new NullPriceHEValue();
        }
    }
}
// lo usamos…
deal.getContratHE().setPrimaryProductPrice(deal.getProductHM(). getPrimaryPrice());

Donde NullPriceHEValue es la implementación NULL de la interface de PriceHEValue y NoValidObjectException no es más que una excepción personalizada lanzada por alguna de las implementaciones nulas del código, por los NULL-Objects.

Vea que los métodos lanzan excepciones y retornan implementaciones para NULL-Object según nos convenga. Recuerde, o lanzamos una excepción temprana o retornamos un NULL-Object.

Veamos otro ejemplo un poquito más largo, favor seguir con atención las declaraciones e implementaciones. Algunos comentarios están en ingles en favor de la internacionalización del código:

//interfaces
public interface Product{
    public String name();
    public HEPrice priceHE();
} 
public interface Products{
    public Product findById(int id);
    public Product add(Product product);
    public Iterator products(); 
}
public interface HEprice{
    public String type();
    public BigDecimal rate();
}
// pigbar rules implementations!! YEAH!!
//product states
public final class ProductNormalState implements Product{
    private final String name;
    private final HEPrice priceHE;
    public ProductNormalState(String name, HEPrice priceHE){
        this.name = name;
        this.priceHE = priceHE;
    }
    public String name(){
        return name;
    }
    public HEPrice priceHE(){
        return priceHE;
    }
}
//product null state
public final class ProductNullState implements Product{
   // just for discipline
    public ProductNullState (){
    }
    // in the case our logic permit an invalid name else throw a NullProductException
    public String name(){
        return “Invalid Product”;
    }
    // depending on the logic can throw an exception or return a HEPriceNullState object
    public HEPrice priceHE(){
        throw new NullProductException(“Null Product, cannot have a priceHE”);
    }
}
// products
// product not validation
Public final class ProductsNoValidation implements Products{
    private final ArrayList products; 
    public ProductsNoValidation(){
        this(new ArrayList());
    }
    public ProductsNoValidation(ArrayList products){
        this.products = products;
    }
    public Product findById(int id){
        if (id <= 0) return new ProductNullState();
        return new ProductNormalState(“Product-” + System.currentTimeMillis(),
            new HEPriceNormalState(“Type-” + + System.currentTimeMillis(), 
            new BigDecimal(System.currentTimeMillis())));
    }
    public Product add(Product product){
        this.products.add(product);
        return product;
    }
    public Iterator products(){
        return products.iterator();
    } 
}
// products with validation, a Decorator for Products, nice!! This allow to decorate with many validatios
Public final class ProductsWithValidation implements Products{
    private final Products products; 
    public ProductsWithValidation(){
        this(new ProductsNoValidation ());
    }
    public ProductsWithValidation(Products products){
        this.products = products;
    }
    public Product findById(int id){
       // just a validation for some id’s values
        if (id >= 10) throw new InvalidParameterException(“Invalid Id for fin a Product, value : ” + id);
        return products.findById(id);
    }
    public Product add(Product product){
        if (product.name().equalsIgnoreCase(“INVALID PRODUCT”))
            throw new InvalidParameterException(“Are you serious? Adding an Invalid Product!!”);
        return products.add(product);
    }
    public Iterator products(){
        return products.products();
    } 
}
// HEPrice implementations, the normal state
public class HEPriceNormalState implements HEPrice{
    private final String type;
    private final BigDecimal rate;
    public HEPriceNormalState(String type, BigDecimal rate){
        this.type = type;
        this.rate = rate;
    }
    public String type(){
        return type;
    }
    public BigDecimal rate(){
        return rate;
    }
} 
// HEPrice null state, not used but nice to see the implementation
public class HEPriceNullState implements HEPrice{
    public HEPriceNullState(){
    }
    public String type(){
        throw new HEPriceNullException(“Null HEPrice cannot have a valid type”);
    }
    public BigDecimal rate(){
                throw new HEPriceNullException(“Null HEPrice cannot have a valid rate”);
    }
}
// now the tester…
public final class Tester{
    public static void main(String[] args){
        new Tester().doTest();
    }

    public void doTest(){
        final Products products = new ProductsWithValidation(new ProductsNoValidation());
        Product productTmp;
        for (int id= -1; id <= 11; id++){
            try{
                productTmp = products.findById(id);
                showProduct(productTmp);
                products.add(product);
            } catch(Exception e){
                Logger.getLogger(“Tester.doTest”).severe(“Error testing produc when Id :  “ + id + ”, with message : ” + e.getMessage());
            }
        }
        showProductsRate(products);
   }
    public void showProduct(Product productTmp){
        try{
            System.out.println(“Product name : ” + productTmp.name());
            System.out.println(“Product price type : ” + productTmp.priceHE ().type());
        } catch(Exception e){
            Logger.getLogger(“Tester. showProduct”).severe(“Error showin produc with message : ” + e.getMessage());
        }    
    }
    public void showProductsRate(Products products){
        while(products.products().hasNext()) {
            Product product = products.products().next();
            System.out.println(“Product name : ” + productTmp.name());
            System.out.println(“Product price rate : ” + productTmp.priceHE ().rate());
        } 
    }
}

Uff…. Código largo pero interesante.
Obviamente ese fue un ejemplo incompleto, pues faltan algunas Excepciones por declarar y algunos thows en los métodos. Lo dejo así para limpieza del ejemplo.

Pero ya puede ver la idea del manejo de NULL en ese código.
Simplemente: NO USE NULL. Period.

Con estos principios ya no tenemos que desvelarnos pensando en cómo evitar las terribles NullpointerException y hacemos nuestro código más robusto y elegante.

No hay comentarios:

Publicar un comentario