miércoles, 25 de agosto de 2010

Code Smells : Obsesión Primitiva

Queridos Chimpances Entrenados, Dice la Enciclopedia Galatica:

Obsesión Primitiva: Es un Smell que se produce cuando uno utiliza tipos primitivos como Strings o Doubles o Ints para representar conceptos que realmente merecen ser representados por clases de objetos.

Es uno de los smells más comunes que podemos encontrar en cualquier código que escribamos o revisemos.

Cuando se trabaja con Objetos, para modelar la estructura interna de una clase uno tiene dos alternativas:


1 - Referenciar a otra Clase de objeto

2 - Utilizar un tipo primitivo.

En general el problema empieza cuando uno decide utilizar un tipo primitivo para modelar un concepto porque se resiste a crear una clase para el mismo porque en ese momento solo sirve para encapsular un simple entero, una String o un par de estos, con nada o muy poco comportamiento extra.

Tipicos ejemplos de esto es cuando uno modela por ejemplo un rango de enteros, como ser, rango de valores válidos para un atributo para un sistema que modele rangos de edades especificos, campos de calle, numero y ciudad para modelar la direccion de un Cliente, etc. . Otro ejemplo es utilizar un int o String para discriminar el "subtipo" o diferentes variaciones de un objeto. Pero el caso emblematico por definición es cuando uno modela sumas de Dinero como un Double para representar el valor, y una String que representa la especie (dolares, pesos, euros, etc.)

Cuando uno sigue trabajando luego de la elección del tipo primitivo al principio siente que ahorro en cantidad de código y clases creadas, y se felicita por ello.

Pronto, no obstante, a medida que el sistema crece comienza a pedir más y más comportamiento de ese tipo primitivo, que, como en general no puede agregarse en el mismo, en lenguajes como Java o C# , se terminan ensuciando el código de la clase adonde se encuentra el primitivo, adonde se agregan métodos y más métodos helpers o utils que trabajan con ese tipo primitivo y con ninguno de los otros atributos de la clase.

Sin entrar en campañas proselitistas, otros lenguajes si permiten agregar métodos no solo a las clases de los primitivos, sino también a instancias específicas (prototype) como por ejemplo en Ruby o Smalltalk, pero no obstante las reglas de Buen Diseño esta indicando la necesidad de un objeto que represente mejor el concepto y que va a permitir agregarle comportamiento a medida que la solución la vaya necesitando (no olvidemos: nunca especular que se necesitará por adelantado).

Ejemplo 1
package ar.com.smells.examples; 
public class ClienteFibertel { 
private String nombre;  
private  String apellido; 
private MedioPago medioPago; 
private long idCliente; 
 
... 
 
private String nombreCalle; 
private String numero; 
private String piso; 
private String Depto; 
private String codigoPostal; 
private String ciudad; 
 
//Metodos propios de clase Cliente 
 
   .... 
 
//Metodos que piden a grito una clase Direccion 
 
    public boolean esCodigoPostalRural() { 
     bla bla bla 
    }
    
    public String direccionFormateadaParaEnvioPostal() {
            bla bla bla 
    } 
 
    public boolean esEdificio() {
           bla bla y mas bla. 
    }
 
} 
En este caso, se deberia refactorizar a una clase Direccion que forme parte del Cliente y que deberia llevarse los tres métodos que estamos indicando que no forman parte de la lógica propia de la clase adonde están.

Ejemplo 2

Un ejemplo muy especial dentro de la obsesion por primitivos, aunque no es el tipico ejemplo, es cuando se utiliza un int como subtipo y esto genera que se repita la lógica de distinción entre subtipos entre todos los métodos de la Clase. La solución es muy clara, la clase debe separarse en una clase base y varias subclases con la diferencia.

 
public class ConexionInternet
{ 
   ... 
 
private int tipoConexion;
 ... 
 
    public String metodoA() { 
         if (tipoConexion == ConexionInternet.CABLEMODEM) { //ya no  hay mas, adios Fibertel
 
         } else if (tipoConexion == ConexionInternet.ADSL) { // speedy? arnet? }
 
           } else if (tipoConexion == ConexionInternet.Modem) { //El unico que va a quedar, la vuelta al 56K! 
 
             }
   } 
 
   public boolean metodoB() {
       if  (tipoConexion == ConexionInternet.CABLEMODEM) { //ya no hay mas, adios Fibertel
     
       } else if (tipoConexion == ConexionInternet.ADSL) { // speedy? arnet? 
 
       
        } else if (tipoConexion == ConexionInternet.Modem) { //El unico que va a quedar, la  vuelta al 56K! 
 
        } 
   }
 
} 

Es bastante lógico decir que parece razonable refactorizar en 3 clases ConexionInternetCableModem, ConexionInternetADSL, ConexionInternetModem, todas dependiendo de una clase abstracta o implementando una interfaz ConexionInternet.

De cualquier manera una de los ejemplos más clásicos y que mayores problemas trae al estar “Obsesionado por Primitivos” es cuando se modelan Sumas de dinero como objetos Double. Los que escribimos este blog damos plena fe de ello.

El primer problema aparece cuando uno comienza a realizar operaciones y surgen las "pequeñas" diferencias por redondeo!. Y claro, cuando uno tiene problemas de redondeo con kilometros entre planetas no hay mucho problema (salvo que este manejando una nave espacial :), pero si es dinero... ahi sii, esos centavos importan y como!

Luego la siguiente estación al desastre es cuando el Usuario, a pesar de habernos dicho que siempre trabajaba con dinero local (pesos, pesos Chilenos, reales, lo que sea) viene muy contentos a contarnos que ahora va a empezar a aceptar pagos en dolares, euros, y en… rupias indias!.

Y nosotros con la clase Producto definida asi:

 
public class Producto {
... 
private double precio; 
 
.... } 

Inmagínense el cambio en todo el sistema!

La solución es ampliamente conocida y no nos vamos a extender en ella por temor a hacer un post kilométrico, pero para los que les interesa pueden verla en detalle en el libro de Test Driven Development (Lo se, lo hemos recomendado muchas veces, de Kent Beck).

Para los ansiosos o remolones, la solucion es crear una clase Money que siga el patrón de diseño Value Object, sin mostrar el detalle de los métodos, sería algo asi:

 
public class Money { 
 
private BigDecimal monto; 
private Currency moneda; 
….
} 

Un hecho relativamente moderno que ha reafirmado e incentivado la Obsesión Primitiva es la utilización másiva de ORMs (Object Relational Mapping), como Hibernate.

El problema en este caso es cuando se esta definiendo un Bean que terminará como tabla en una base de datos y, y condicionados por la complejidad de la herramienta, se prefiere tener un bean al cual le agregan los atributos primitivos directamente y no múltiples tablas de objetos relacionados.

Por ejemplo, prefieren tener una sola clase SujetoAnses al cual le agreguen 2 int para guardar rango máximo y mínimo configurado a tener otra tabla relacionada con el Bean principal con los rangos permitidos.

O del ejemplo anterior, ClienteFibertel con todas las String en una misma tabla a tener una relacion con otra tabla Direccion. Es inútil explicar en estos casos que en general no es razonable decidir el Diseño de Objetos por las consecuencias que tendrán sobre la base de datos. Al menos no a priori hasta tener razones de peso, de performance, etc..

Para terminar, no estamos afirmando que TODO atributo o concepto simple debe ser representado como una clase de objetos que se convierta en simples wrappers de un int, String o double.

Pero ante el minimo comportamiento que haya que agregar, cuando uno comienza a pasar ese valor de aquí para allá o hace algún método que lo utiliza solo a él, etc., en esos casos mantenerlo como primitivo es… una obsesión y hay que tratarla!.

No hay comentarios:

Publicar un comentario