11 de noviembre de 2010

Delegación simple con Python

Al estudiar el libro "Design Patterns: Elements of Reusable Object-Oriented Software", nos damos cuenta que frecuentemente debemos delegar métodos desde una clase "envoltorio" hacia una instancia de una clase "envuelta" (o "sujeto", para usar la terminología de los GOF). Por ejemplo los patrones decorador y proxy son dos ejemplos en los que se requiere hacer esto.

Sin embargo, una situación recurrente al usar estos patrones es que en realidad no queremos redefinir o controlar todos los métodos del sujeto. Algunas veces, solo queremos "decorar" o controlar ciertos métodos y dejar el resto en paz. En lenguajes como Java, C# o (Dios nos guarde) Visual Basic, esto implica escribir una larga lista de métodos que lo único que hacen es delegar hacia el sujeto.

Así, si tenemos una clase a la que nos gustaría decorar, pero únicamente queremos decorar un único método, en Java tendríamos algo así:

interface MySubject
{
  void decorated(char x);
  void foo();
  long bar(int blah);
  int baz(int x, float y, char z)
  ...
}

class MyDecorator implements MySubject
{
  public MyDecorator(MySubject subject)
  {
     this.subject = subject;
  }

  public void decorated(char x) {
    if (x != 'x') {
      self.addBehaviour();
    }
    this.subject.decorated();
    self.addMoreBehaviour();
  }

  private void addBehaviour() { ... }

  private void addMoreBehaviour() { ... }

  public void foo() {
    this.subject.foo();
  }

  public long bar(int blah) {
    return this.subject.bar(bla);
  }
  ....
}

etc.

En definitiva es mucho trabajo. En python es tan sencillo como definir un solo método (aunque, hay que decirlo, se trata de un método "mágico"). Dicho método es __getattr__. Este método es llamado por python cada vez que la búsqueda de un atributo en un objeto falla. Es decir, si nosotros escribimos:

foo.bar()

Pero el método bar no existe en la clase del objeto foo, entonces obtendremos una linda excepción AttributeError. Sin embargo, python nos da una oportunidad de atrapar estos "eventos" antes de levantar la excepción. Si nuestra clase define el método mágico __getattr__, python lo mandará llamar cada vez que no pueda encontrar el atributo solicitado.

Esto significa que nuestro decorador en java se simplifica significativamente al implementarlo en python:

class MyDecorator(object):
    def __init__(self, subject):
        self._subject = subject

    def decorated(self, x):
        if x != 'x':
            self._addBehaviour()
        self._subject.decorated()
        self.:addMoreBehaviour()

    def _addBehaviour(self):
        ...

    def _addMoreBehaviour(self):
        ...

    def __getattr__(self, name):
        return getattr(self._subject, name)

¡Eso es todo!

  1. En primer lugar en python no necesitamos definir una interfase ya que utiliza duck typing.

  2. En segundo lugar, con solo definir __getattr__, automáticamente estamos delegando todos los métodos que no definimos explícitamente hacia _subject,

De esta forma, si escribimos:

myobj = MyDecorator( SomeObject() )
myobj.decorated('x')

pasará exactamente lo mismo que en la versión Java. Sin embargo, si escribimos esto:

myobj = MyDecorator( SomeObject() )
print myobj.bar(25)

python tratará de invocar el método bar en myobj. Como ese método no existe en la clase MyDecorator, python seguidamente buscará el método __getattr__. En este caso, la búsqueda tiene éxito por lo que __getattr__ es invocado. Este a su vez nos regresa una referencia al método bar de la clase SomeObject y finalmente python puede llamar al método.

¿Pero, qué pasa si el método llamado tampoco existe en el sujeto? Bueno, el contrato para __getattr__ indica que de no encontrar el atributo solicitado, se debe levantar una excepción AttributeError. Y eso es exactamente lo que hace la función getattr si solicitamos un atributo que no existe, por lo tanto, cumplimos al 100% con nuestro contrato, tanto con python como con el código cliente, que ni siquiera se entera que no está hablando con una instancia de SomeObject real.

Esto es tan útil que he hecho una pequeña clase de utilería para usarla una y otra vez:

class SimpleDecorator( object ):
    u'''
    Es un "mixin" que ayuda en la implementación de patrones de diseño, al 
    permitir la delegación automática de los mensajes que el objeto no 
    entienda hacia otro objeto "subject".
    '''
    def __init__( self, subject ):
        self._subject = subject

    def __getattr__( self, name ):
        return getattr( self._subject, name )

Saludos

14 de junio de 2010

To override or not to override?... ya no es un dilema

Ok, continuando con las penurias de nuestro último proyecto en Python y sobre cómo el uso de TDD puede hacerlas más llevaderas, hoy quiero mostrar una técnica que me resulta muy útil.

Normalmente no me gustan las entradas del tipo "receta de cocina", creo que ya hay varios blogs dedicados a ese tipo de tips, sin embargo, como "blog" viene de "log" que a su vez es "bitácora", pues bueno, usaré este espacio como dicha bitácora para registrar lo que voy encontrando: lo bueno, lo malo y lo feo (que es mucho), para que después no se me olvide y, si de paso a alguien más le resulta útil, pues qué mejor.

Bueno, entrando en materia en el post anterior les platicaba yo cómo comenzar a agregar pruebas de caracterización en código legacy. Mientras me encontraba haciendo dichas pruebas, me topé con que el código en cuestión genera un archivo de .xls, mismo que contiene una página de resumen y tres páginas de detalle, donde se guardan los registros que se procesaron correctamente, los que no se pudieron procesar porque falta información y los que tuvieron errores de cálculo (imaginense nomás).

Bueno, como PyDev en realidad hace un trabajo bastante decente con el refactoring "Extract Method", me colgué de allí para encontrar cúmulos de lineas relacionadas e irlas extrayendo en rutinas. Al final terminé con una pequeña clase que escribe la información en las hojas de excel según se va procesando. Ahora lo que necesitaba era poder "detectar" esa información para poder verificar que el proceso fuera correcto.

Ya anteriormente mencioné lo facil que es crear una sub-clase en Python para propósitos de prueba, con el fin de poder redefinir ciertos métodos. También mencionamos que para los casos en los que solo necesitamos redefinir un método, existen otras alternativas.

En Python, es posible alterar la estructura de un objeto en tiempo de ejecución, por lo que facilmente podemos sustituir el método que necesitamos por otra cosa distinta. Veamos un ejemplo un poco más concreto:

Como ya tenemos una clase GeneradorTesting, me cuelgo de ahí para crear un "Factory Method" que cree nuestro libro de excel: Utilizando "Extract Method" con PyDev en la clase GeneradorArchivosobtengo:

class GeneradorArchivos( object ):
 def _creaWorkbook( self, nombre ):
   return MyWorkbook( nombre )

MyWorkbook es la clase que cree como interfaz con el código de creación del archivo .xls. Ahora, planeo que cada vez que se agregue un registro a una hoja de mi libro, agregar dicho registro a una lista en donde poder verificarlo posteriormente. Tambien quiero tener una lista separada para cada hoja. Para esto, creo un diccionario vacio en mi sub-clase de pruebas:

class GeneradorArchivosPrueba( GeneradorArchivos ):
 def __init__( self, frame, batch_id ):
   super( GeneradorArchivosPrueba, self ).__init__( frame, batch_id )
   self.registros = {}

Bien, ahora tengo un problema: la creación de hojas individuales es una responsabilidad de MyWorkbook, no de GeneradorArchivos (que es en parte la razón por la cual creé esa clase en primer lugar). Por lo tanto, no puedo redefinir un método en GeneradorArchivosPrueba para hacer esto.

Podría crear una sub-clase de MyWorksheet llamada MyTestingWorksheet y despues redefinir el método _creaWorkbook para que regrese una sub-clase de MyWorkbook, que a su vez devuelva instancias de MyTestingWorksheet, pero creo que resulta demasiado lio para lo que yo necesito. Parece ser que estoy atorado.

Bueno, en realidad no. Gracias a algunas características muy útiles de Python, como el que pueda redefinir ciertas cosas al vuelo, puedo crear instancias "personalizadas" de las clases de "producción" y usarlas en mis pruebas. Veamos:

Lo primero que necesito es que la creación de los objetos hoja resida en su própio método:

class MyWorksheet( object ):
 ...
 def _creaHoja( self, titulo ):
   hoja = self._libro.add_worksheet( titulo )
   ...
   return hoja

Como siempre, corremos las pruebas que ya teníamos para verificar que lo demás siga funcionando. Después, redefinimos nuestro Factory Method para objetos libro:

class GeneradorArchivosPrueba( GeneradorArchivos ):
 ...
 def _creaWorkbook( self, nombre ):
   workbook = GeneradorArchivos._creaWorkbook( nombre )
   return workbook

Ahora, después de volver a correr las pruebas, ya que puedo controlar la creación de libros. Personalicemos dichos objetos para nuestras pruebas:

class GeneradorArchivosPrueba( GeneradorArchivos ):
 ...
 def _creaWorkbook( self, nombre ):
   workbook = GeneradorArchivos._creaWorkbook( nombre )
   old_crea_hoja = workbook._creaHoja

   def nuevaCreaHoja( *args, **kwds ):
     return old_crea_hoja( *args, **kwds )

   workbook._creaHoja = nuevaCreaHoja
   return workbook

Probamos y vemos que las cosas parecen seguir funcionando hasta aquí... ¡¡fiiuuu!!

En este punto, aún no he cambiado el comportamiento del código, sin embargo, ya estoy en posibilidad de hacerlo.

Antes de continuar, me gustaría llamar su atención a la variable old_crea_hoja. Aquí estamos guardando una referencia a la definición original del método creaHoja. Sin embargo, si miramos con cuidado, vemos que es una variable local... ¡¡se pierde al momento de salir de _creaWorkbook!! Bueno, no en realidad.

Lo que sucede aquí es que Python está creando un closure para la función nuevaCreaHoja. En términos simples, lo que esto significa es que el contexto de ejecución de nuevaCreaHoja es guardado y después recuperado cuando se llama a la función, por lo que las variables locales de la función superior a las cuales nuevaCreaHoja tiene acceso, conservan su valor cada vez que esta se ejecuta. Esto incluye a old_crea_hoja por supuesto, y esto es lo que nos permite llamar al método original.

Bueno, ahora necesitamos agregar elementos a nuestro diccionario registros cada vez que se agregue una hoja:

class GeneradorArchivosPrueba( GeneradorArchivos ):
 ...
 def _creaWorkbook( self, nombre ):
   workbook = GeneradorArchivos._creaWorkbook( nombre )
   old_crea_hoja = workbook._creaHoja

   def nuevaCreaHoja( *args, **kwds ):
     hoja = old_crea_hoja( *args, **kwds )
     self.registros[hoja.titulo] = []
     return hoja

   workbook._creaHoja = nuevaCreaHoja
   return workbook

¡Perfecto! Ahora (después de probar, claro está), podemos aplicar exactamente la misma técnica para lograr que al agregar registros a cada hoja se registren en la lista que les corresponde:

class GeneradorArchivosPrueba( GeneradorArchivos ):
 ...
 def _creaWorkbook( self, nombre ):
   workbook = GeneradorArchivos._creaWorkbook( nombre )
   old_crea_hoja = workbook._creaHoja
     def nuevaCreaHoja( *args, **kwds ):
       hoja = old_crea_hoja( *args, **kwds )
       self.registros[hoja.titulo] = []
       old_agrega_registro = hoja.agregaRegistro
       def nuevaAgregaRegistro( *args ):
           self.registros[hoja.titulo].append( args )
           return old_agrega_registro( *args )
       hoja.agregaRegistro = nuevaAgregaRegistro
       return hoja
   workbook._creaHoja = nuevaCreaHoja
   return workbook

¡Puf, casi terminamos!. Ahora todo lo que nos hace falta es verificar la información que estamos escribiendo a la hoja de cálculo en nuestra suite de pruebas. Para ello, genero la hoja correspondiente con la versión sin modificar de la aplicación y a partir de esta obtengo mi información de referencia:

class TestGeneradorArchivos( ConnectedDatabaseTest ):
 def checaNegativos( self, *esperados ):
   for esperado in esperados:
     assert esperado in self.gen.registros["Negativos"]

 def testGenera9981( self ):
   """Genera los archivos de pago y los compara contra un set de 'buenos conocidos'."""
   self.gen.genera(9981)
   assert self.gen.registros_totales == 82
   assert self.gen.registros_negativos == 2
   self.checaNegativos( ('9981',1834,'GOMEZ, PACO','',516.34),
                        ('9981',2359,'DOE, JOHN','',1677.42') )

Saludos y hasta la próxima

8 de junio de 2010

Porque TDD no es opcional

Como probablemente sabrán, Alcides y un servidor hemos estado los últimos meses dándole mantenimiento a una aplicación legacy en python. Bueno, creo que es una buena oportunidad de comentar algunos de los problemas más duros con los que nos hemos topado y, como a pesar de ser problemas comunes en la mayoría de los proyectos, al estar en un lenguaje dinámico como python estos se exacerban.

Creo que el problema principal es el excesivo nivel de acoplamiento presente en la totalidad de la aplicación. El siguiente ejemplo fue extraído del código de un proceso por lotes que genera una serie de archivos TXT para realizar transferencias electrónicas masivas:

  numero_cuenta, nombre_beneficiario = self.frame.obtenDatosBanco( str( numero_empleado ) )

nota: el código ha sido modificado para no comprometer la confidencialidad del cliente y para no ofender los castos ojos del amable lector con detalles de formato, nomenclatura, estilo, etc., presentes en el original)

En el ejemplo anterior, self.frame se refiere a una referencia que guarda el objeto a la forma que lo invocó. Desde ahí uno se puede preguntar ¿Porqué un objeto que ejecuta un proceso batch requiere una referencia a la interfaz de usuario?. Más aún, ¿Porqué ese elemento de la GUI implementa un método que claramente pertenece al dominio del negocio (obtenDatosBanco)?

Al analizar el objeto en cuestión, nos dimos cuenta que las llamadas a self.frame.* se encuentran por todas partes en el código. Prácticamente no había manera de saber qué podíamos afectar si cambiábamos el batch o la ventana.

Una complicación adicional es que python, al ser un lenguaje interpretado, no genera errores de compilación u otra clase de advertencia si, por algún descuido, modificamos adversamente las dependencias del código mientras lo modificamos. Si bien es cierto que existe una herramienta (¡Muy útil!) que nos asiste en el proceso, llamada pylint, lo cierto es que aún así es necesario leer y re-leer el código varias veces tomando notas intensivamente y tratando de descubrir todos los posibles efectos que pueda tener una modificación.

Ahora, si bien en cualquier pieza de código el proceso anterior resulta extenuante, cuando se trata de un mega-archivo fuente de al rededor de 40,000 líneas de código y con un nivel extremadamente alto de acoplamiento, es poco decir que la tarea se vuelve insufrible.

Existe una técnica que nos ha dado resultado, llamada "Pruebas de Caracterización", documentada por Michael Feathers en su libro "Working Effectively with Legacy Code". Básicamente, lo que hacemos es crear un arnés de pruebas alrededor del código que deseamos modificar. La diferencia con las pruebas unitarias tradicionales es que en este caso, las pruebas no pueden ser escritas antes que el código (obvio), por lo tanto no pueden ser una especificación de lo que el código debe hacer. En cambio, las pruebas documentan lo que el código hace actualmente.

Esto significa que es probable que el código actual contenga bugs. Sin embargo, nuestro arnés de pruebas puede o no detectar dichos bugs, ya que no es esa su función. En cambio, su función es la de hacer visible el comportamiento actual del código, de tal suerte que podamos hacer las modificaciones necesarias y aún estar razonablemente seguros de que el resto aún funciona. Lo que es más, el tener nuestras Pruebas de Caracterización en su lugar, ¡No excluye el escribir pruebas unitarias para el código nuevo que se deba escribir!

Usualmente la prueba más difícil es la primera: crear una instancia del objeto en cuestión dentro de nuestro arnés. En este caso, es algo así:

from pago_electronico import GeneradorArchivos

class TestGeneradorArchivos( TestCase ):
    def setUp( self ):
        self.gen = GeneradorArchivos( frame=None, batch_id=None )
    def testCrea( self ):
        """Verifica que sea capaz de crear una instancia en el ambiente de pruebas"""
        self.assertIsNotNone ( self.gen )

Inicialmente, la prueba falla, ya que se requiere pasar una instancia del frame antes mencionado para inicializar la instancia. Sin embargo, no queremos crear una instancia de ese frame para esta prueba ya que por una parte, es una clase monstruo de arriba de 11 mil lineas de código que a su vez incluye toneladas de dependencias con ella. Por otra parte, requeriría la inicialización del sistema wxPython para el manejo de interfaz de usuario, lo que nos implicaría inicializar una instancia de wx.App... bueno, creo que ustedes captan la idea.

En su lugar, lo que queremos hacer es sustituir esa referencia a frame con otra cosa que se más fácil de crear y que podamos controlar.

Una de las propiedades de frame que utiliza GeneradorArchivos es connection, que regresa una referencia a la conexión con la base de datos. Anteriormente había desarrollado una sub-clase de TestCase para esos caso (con el plus de que esta última se asegura de abrir y cerrar la conexión cuando sea necesario, evitando la fuga de recursos). Así, lo primero que hago es cambiar TestGeneradorArchivos para ser un descendiente de mi clase:

from pago_electronico import GeneradorArchivos
from tests.custom import ConnectedDatabaseTest

class TestGeneradorArchivos( ConnectedDatabaseTest ):
    def setUp( self ):
        super( TestGeneradorArchivos, self ).setUp()
        self.gen = GeneradorArchivos( frame=self, batch_id=None )

La llamada al método setUp de la superclase es necesaria para asegurarnos que la inicialización de la conexión se haga correctamente. Noten que también se pasa self como el parámetro frame.

Ahora bien, para todos los que leen esto que están acostumbrados a C#, Java, etc., dirán "eso es un error". Claramente, en esos lenguajes no podría hacer esto, ya que TestGeneradorArchivos no es de la clase adecuada que espera el constructor de GeneradorArchivos. Sin embargo, recordemos que:

Python es un lenguaje dinámico al que no le importa mucho la clase particular a la que pertenecen nuestros objetos, sino su tipo.

¿Su tipo?, ¿Que, no es lo mismo? er... no.

  • La clase de un objeto es el molde a partir del cual se crean instancias del mismo. Todo lo que tiene la clase terminará siendo parte de los objetos que a partir de ella se creen.

  • El tipo de un objeto esta definido por el conjunto de mensajes a los que dicho objeto puede responder.

En los lenguajes estáticos como Visual Basic, Java, C#, etc., la clase de un objeto define también su tipo, ya que también define los mensajes a los que ese objeto responderá. En dichos lenguajes y otros similares, clase y tipo son conceptos inseparables (o casi, si consideramos el uso de interfaces).

Por otro lado, en python podemos pasar una referencia a cualquier objeto como parámetro de una función y esta no se inmutará. Lo único que importa es que si la función llama a foo.bar(), el objeto que pasemos en el parámetro foo responda al mensaje bar.

De vuelta a nuestro ejemplo, vemos que la primera prueba pasa. Hora de escribir una segunda prueba. En este caso, las pruebas de caracterización son más como mini-pruebas de integración, ya que describen escenarios en lugar de solo verificar el resultado de una sola operación. Veamos:

    def testGenera( self ):
        """Genera los archivos de pago y los compara contra un set de 'buenos conocidos'."""
        self.gen.genera()

Esta prueba falla inmediatamente, ya que al no especificar un batch_id, la clase lanza una caja de dialogo para obtener dicho ID del usuario. Sin embargo, como no hemos incluido wxPython en nuestra prueba, levanta una excepción.

Esto se arregla fácilmente pasando la información requerida en el constructor:

    def setUp( self ):
        super( TestGeneradorArchivos, self ).setUp()
        self.gen = GeneradorArchivos( frame=self, batch_id=1234 )

Claro, primero nos aseguramos que el batch 1234 exista y cumpla con los requisitos que necesitamos para nuestra prueba.

Ahora la prueba falla, pues está intentando acceder al método GetCurrentDate del frame. De nuevo, ¿que tiene que hacer un método como GetCurrentDate ahí, no lo sé. Sin embargo, le daremos lo que requiere. Como estamos pasando una referencia a self en el constructor, crearemos un método stub en TestGeneradorArchivos:

class TestGeneradorArchivos( ConnectedDatabaseTest ):
    def GetCurrentDate( self ):
        return ( 1, 3, 2010 )

Y así sucesivamente.

Otro problema con el que nos topamos es que repetidamente GeneradorArchivos intenta crear cajas de diálogo para mandar mensajes al usuario o pedir información del mismo. Normalmente mientras estamos haciendo pruebas de caracterización, no hacemos refactorings. Esto es, porque para hacerlos correctamente, ¡Necesitamos pruebas que cubran el código que queremos refactorizar! Es como el problema de la gallina y el huevo.

Sin embargo, en este caso los refactorings son extremadamente simples y podemos realizarlos con un nivel aceptable de seguridad:

class GeneradorArchivos( object ):
    def obtenPrimerSecuencial( self ):
        return wx.GetTextFromUser( caption=u"Número Secuencial", message=u"Digite el Número Secuencial a usar", default_value="1" )

    def genera(self):
        ...
        inicial = self.obtenPrimerSecuencial()
        ...

A continuación, podemos aplicar la técnica estándar de sustitución de métodos en una prueba unitaria:

class GeneradorArchivosPrueba( GeneradorArchivos ):
    def __init__( self, frame, batch_id ):
        super( GeneradorArchivosPrueba, self ).__init__( frame, batch_id )
    def obtenPrimerSecuencial( self ):
        return "1"

class TestGeneradorArchivos( ConnectedDatabaseTest ):
    def setUp( self ):
        super( TestGeneradorArchivos, self ).setUp()
        self.gen = GeneradorArchivosPrueba( frame=self, batch_id=1234 )

En este caso, hay otros métodos que queremos redefinir en GeneradorArchivos, por lo que conservaremos la clase GeneradorArchivosPruebas. Sin embargo, si solo necesitáramos redefinir obtenPrimerSecuencial o un par de métodos simples más, existe una forma más sencilla de hacerlo:

class TestGeneradorArchivos( ConnectedDatabaseTest ):
    def setUp( self ):
        def regresaUno(): return "1"
        super( TestGeneradorArchivos, self ).setUp()
        self.gen = GeneradorArchivos( frame=self, batch_id=1234 )
        self.gen.GeneradorArchivosPruebas = regresaUno

o incluso:

class TestGeneradorArchivos( ConnectedDatabaseTest ):
    def setUp( self ):
        super( TestGeneradorArchivos, self ).setUp()
        self.gen = GeneradorArchivos( frame=self, batch_id=1234 )
        self.gen.GeneradorArchivosPruebas = ( lambda "1" )

Ahora que ya logramos ejecutar correctamente a GeneradorArchivos en un arnés de prueba, ¿qué probamos? Uno de los métodos que extrajimos al eliminar el uso de diálogos fue muestraResumen, que simplemente muestra un resumen del trabajo realizado por el batch:

class GeneradorArchivos( object ):
    ....
    def muestraResumen( self, registros_totales, registros_negativos ):
        Mensajes.MuestraAviso( FMT_RESUMEN % ( registros_totales, registros_negativos ), parent=self.frame )

bueno, podemos utilizar este método para verificar estos resultados:

class GeneradorArchivosPrueba( GeneradorArchivos ):
    registros_totales = 0
    registros_negativos = 0
    def muestraResumen( self, registros_totales, registros_negativos ):
        self.registros_totales = registros_totales
        self.registros_negativos = registros_negativos

class TestGeneradorArchivos( ConnectedDatabaseTest ):
    def testGenera( self ):
        """Genera los archivos de pago y los compara contra un set de 'buenos conocidos'."""
        self.gen.genera()
        assert self.gen.registros_totales == -1
        assert self.gen.registros_negativos == -1

Esto nos da un primer acercamiento a las pruebas de caracterización. Al correr el proceso batch en una base de datos de prueba contra el batch número 1234, descubrimos que procesa un total de 36 registros, de los cuales 4 son negativos. Como nuestra prueba debe describir lo que el programa hace actualmente, incluimos estos resultados en nuestra prueba:

class TestGeneradorArchivos( ConnectedDatabaseTest ):
    def testGenera( self ):
        """Genera los archivos de pago y los compara contra un set de 'buenos conocidos'."""
        self.gen.genera()
        assert self.gen.registros_totales == 36
        assert self.gen.registros_negativos == 4

Este proceso continúa hasta que nos sentimos satisfechos con nuestras pruebas de caracterización, rompiendo dependencias y detectando resultados. A continuación pudimos hacer las modificaciones que se nos pidieron y de paso pudimos darle una muy necesaria limpieza al código—no tanto como hubiésemos querido, pero es un comienzo.

Saludos y espero que haya sido de su interés.

27 de mayo de 2010

GW-Python??

No, no se confundan por el titulo. En realidad, no creo que python y gwbasic se parezcan o sean incluso comparables. Me explico:

Como muchos otros programadores, my primer contacto con la programación fue con el humilde interprete de basic. Y como muchos otros, una vez que aprendí lo básico, rápidamente me di de frente con las limitaciones del mismo: sin verdaderas sub-rutinas, todas las variables globales, GOTOs como la forma casi exclusiva de control de flujo, las variables se crean al vuelo y se les puede asignar cualquier cosa, etc.

En esas condiciones, resultaba casi imposible escribir algo mas grande y complejo que los programitas de muestra de los libros de Joyanes sin perder la razón en el intento.

Debido a la cultura que prevalece en la comunidad de python en la que hacen mucho énfasis en la legibilidad, fue para mi una sorpresa toparme con una base de código "legacy" que me hizo recordar aquellos lejanos dias de gw-basic. Increíblemente, algunos "programadores" hoy en dia siguen pensando que TODO el código del sistema ha de estar en un solo ENORME programa sin sub-rutinas (o casi), variables globales y creadas al vuelo sin ton ni son, etc. De hecho, en este codigo he encotrado ejemplos de todas y cada una de la peores practicas del libro y creo que incluso otras nuevas. Todo excepto los GOTOs endémicos de basic...

Pero acaso encontramos algo aun peor: encerrar en bloques try...except casi una linea de código si y otra no, con el fin de que el programa no "truene"... No, que va, solamente se limita a hacer pedasitos los datos del cliente, en silencio y bajo la mesa, arrojando ceros como resultado de forma semi-aleatoria en lugares que hacen parecer que los resultados son validos a simple vista (y milagrosamente, ¡A veces lo son!)

En conclusión, ya antes he hablado aqui de las bondades de los lenguajes como python o ruby. Por la enorme comunidad de desarrolladores que se ha formado a su alrededor y por las muestras de verdadero orgullo, ética y cultura que abundan en dichas comunidades y por el software increíble que ha salido de las mismas (resultado de código de muy alta calidad), me había hecho yo a la idea de un mundo de ensueño en donde no existe el viejo adagio de programación: "se puede escribir en fortran usando cualquier lenguaje".

Python es un lenguaje muy flexible y poderoso que nos permite hacer en cuestion de dias lo que en java o C# nos tomaría semanas. Sin embargo, no debemos olvidar las sabias palabras de Stan Lee, en boca de Peter Parker: "con un gran poder viene una gran responsabilidad"--claro, a menos que te guste trabajar con código que haga que te tome semanas lo que pudo bien llevarte unos días.

Saludos

26 de febrero de 2010

MSF for Agile y XP

Recientemente Alcides me mandó un correo en donde una persona pregunta sobre las similitudes y diferencias entre MSF for Agile y XP, y si es posible usarlos juntos o usar XP con la suite de herramientas de Microsoft.

Antes de entrar en tema me parece correcto aclarar que MSF for Agile no es una metodología, ni un proceso de desarrollo--como si lo es XP. MSF for Agile es lo que Microsoft denomina un “marco de soluciones”. Es decir, se limita a proporcionar a sus usuarios con algunos artefactos, modelos, conceptos y guías.

En particular, si se observa el MSF for Agile Software Development Process Template y el MSF for Agile Software Development Process Guidance, uno puede ver de inmediato que están pensados para usarse con Scrum. Aunque se supone que MSF es agnóstico en cuanto a metodologías se refiere, existe una muy fuerte influencia de RUP y de Scrum.

Ahora bien, desde ahí existe una diferencia sin necesidad de entrar en más detalle. Hablando llanamente, Scrum y XP fueron pensadas y diseñadas desde dos puntos de vista diferentes:

  • Scrum está pensado como un proceso ágil de administración de proyectos, mientras que
  • XP está pensado como un proceso ágil de desarrollo de software.

De hecho, en cierta forma se puede pensar en ellos como procesos ortogonales:

  • Scrum se ocupa primordialmente de los aspectos de gestión y comunicación del proyecto e intencionalmente deja fuera los aspectos referentes a las prácticas técnicas específicas usadas durante el proyecto.
  • XP por otra parte, se concentra en los aspectos más técnicos de la ejecución del desarrollo. Aunque XP contiene prácticas orientadas a la planeación y administración del proyecto, estas tradicionalmente han sido poco explicadas y por ende, poco comprendidas.

Un dato relevante al respecto es que de hecho, Ken Schwaber recomienda el uso de las prácticas de ingeniería de XP (ver figura) cuando se desarrolla un proyecto de software usando Scrum.

circles

Así, mientras que Scrum puede ser usado para gestionar cualquier clase de proyecto, XP es específicamente para el desarrollo de software. Es por ello que Scrum es tan aludido recientemente: Es más fácil de entender para los gerentes y demás personas de perfil no-necesariamente-técnico.

En cuanto a su relación con MSF, éste principalmente se dedica a promover algunas prácticas específicas para el uso con productos de Microsoft durante la mayor parte del ciclo de vida del desarrollo. Provee de guías, plantillas de documentos, sitios web, y demás cosas para aplicarlas durante el proyecto.

¿No tienes un formato oficial para casos de uso? MSF for Agile Software Development Process Template trae plantillas en formato Word.

¿Necesitas realizar un documento de Backlog para tu proyecto/sprint? MSF for Agile Software Development Process Template trae plantillas de Excel para ello.

¿Necesitas un sitio de colaboración para tu equipo de trabajo? MSF for Agile Software Development Process Template contiene una plantilla de sitio Web para Sharepoint, incluyendo blog, wiki y otras monerías.

Además, Microsoft Team System incluye software para control de versiones, seguimiento de pendientes, integración continua, reportes, etc.

Es verdad que Microsoft ha incluido varias características en sus productos que pueden resultar atractivas a los equipos de desarrollo ágiles, pero en ningún momento se puede decir que se trate de productos para el desarrollo ágil.

Un proyecto ágil puede llevarse a cabo igualmente usando herramientas Open Source para las mismas tareas:

Y un largo, largo etcétera.

Microsoft ha integrado documentación, plantillas, artefactos y herramientas de desarrollo en un paquete que espera que sea lo suficientemente atractivo para que los desarrolladores decidamos usarlo en vez de alguna otra alternativa.

Por lo tanto, si lo que estamos tratando de resolver es cuál método usar para nuestros proyectos, la pregunta MSF o no-MSF no es siquiera relevante. Primero debemos revisar los principios detrás de los métodos que estamos evaluando y ver qué tanto se adaptan dichos principios a la cultura actual de mi empresa, mi equipo de trabajo (o a lo que quiero que llegue a ser dicha cultura) y a las necesidades de mi proyecto.

Si nos decidimos por usar Scrum o RUP y además estamos usando o planeamos usar la suite de herramientas de Microsoft, entonces es conveniente darle un vistazo a MSF, siempre recordando que este no es un fin en si mismo, sino una herramienta más que habrá de usarse y adaptarse a mis necesidades para resolver el problema que tengo entre manos.

Saludos.

24 de febrero de 2010

¡¡Polimorfismo, polimorfismo, polimorfismo!!

Quizás la característica más importante de los entornos orientados a objetos sea el poco comprendido y aún menos usado polimorfismo.

No me malinterpreten: la encapsulación es importante, pero ya COBOL y sus módulos podían esconder los datos mediante interfaces "bien definidas".

En un lenguaje orientado a objetos, la encapsulación no es sino aplicar los principios de modularidad hasta sus últimas consecuencias

Lo que es más, es probable —y muchas veces aún deseable— romper la encapsulación de un objeto cuando su único propósito es el trasporte de datos. Esto es tan común que incluso hay un patrón que lo documenta: DTO (Data Transfer Objects)

Sin embargo, el tema que quiero tocar el día de hoy es uno que consistentemente encuentro que los sistemas legacy pasan por alto: el polimorfismo.

Como una definición simple diremos que el polimorfismo es la capacidad para mandar un mismo mensaje a varios objetos de —potencialmente— distintos tipos y que estos procesen dicho mensaje de la forma más adecuada posible.

Por ejemplo, en Java o .NET podemos llamar al método ToString de cualquier objeto sin importar su tipo, y obtener una respuesta adecuada. Así pues, si el objeto es el Int32 13, obtendremos la cadena "13"; si el objeto es un registro de un sistema de crédito, tal vez obtengamos una cadena con el nombre del acreditado y su saldo al corte; si el objeto es un String, entonces obtendremos el mismo objeto como respuesta.

Como ven, cada objeto responde de manera distinta a la invocación de ToString, pero siempre de manera consistente con:

  1. Lo que se espera de dicho método (una representación de cadena del objeto en cuestión), y
  2. La naturaleza del objeto mismo (sus reglas de negocio, etc.)

Si el polimorfismo es tan maravilloso, ¿Porqué nadie lo usa?

Aquí quisiera hacer una aclaración. Como lo demuestra el ejemplo anterior, en realidad usamos el polimorfismo todo el tiempo. Sin embargo, como desarrolladores no hemos asimilado el pensamiento en objetos y seguimos anclados a las mismas técnicas y los mismos paradigmas de antaño. Nadie extraña lo que no ha tenido. Si nunca hemos aprendido a utilizar correctamente una herramienta y a dominarla, seguiremos naturalmente a la tendencia de usar aquellas que mejor conocemos y que nos han funcionado en el pasado.

Ahora, no quiero aquí discutir si las técnicas tradicionales funcionan o no. Es claro que funcionan puesto que se siguen usando. El punto es qué tan adecuadas son para la tarea que les hemos asignado.

Como comenté al principio del blog, algo que consistentemente veo en sistemas legacy (¡¡incluso algunos que se desarrollaron hace 2 meses!!) es la completa falta de polimorfismo en su diseño e implementación. ¿Cual es la alternativa escogida en su lugar? Series interminables de IFs y SELECT CASE/SWITCH regadas prácticamente por la totalidad de la aplicación. Algo parecido a esto:

public Money GetSaldo() {
  ...
  if (Cuenta.Tipo == C_CORRIENTE) {
    Interes = [calcular el interés de cuentas corrientes]
  } else if (Cuenta.Tipo == C_CHEQUES) {
    Interes = [calcular el interés de cuentas de cheques]
  } else {
    Interes = [calcular el interés de cuentas "comodity"]
  } 

  Saldo += Interes;

Al final, vemos listas interminables de condiciones que "checan" el tipo de cuenta para la aplicación de muy diversas reglas de negocio, diseminadas por todo el código sin un lugar bien definido para su definición y su aplicación.

Lo que un código como el anterior nos está diciendo prácticamente a gritos es que existen (por lo menos) tres tipos de cuentas y cada uno tiene comportamientos bien diferentes en contextos específicos.

Algunos podrán pensar ¡Ah! entonces hay que heredar tres subclases de Cuenta. Pues no, o por lo menos no necesariamente. Efectivamente, en lenguajes que la soportan, podemos usar herencia como nuestro vehículo para crear objetos polimórficos, sin embargo, algo que en su momento me costó trabajo comprender (probablemente por mis raizes de lenguajes fuertemente tipados) es que el polimorfismo puede operar de forma independiente a la herencia.

Si estamos trabajando en Java o .NET, es posible utilizar una interface. En C++ y Visual Basic 5 o 6, usamos una clase abstracta, es decir, una que solo contiene métodos virtuales sin código. En Smalltalk, Ruby, Python, Javascript, Objective-C y —sorpresa, sorpresa— de nuevo, Visual Basic 5, 6 o VBscript, etc., podemos enviar mensaje y dejar que el sistema de mensajería del lenguaje resuelva la invocación del método apropiado.

Regresando al ejemplo anterior, suponiendo que tenemos tres clases que implementan el cálculo del interés de sendos tipos de cuenta y que responden adecuadamente al mensaje GetInteres, podemos reescribirlo de la siguiente forma:

interface Rendimientos {
  Money GetInteres(Cuenta c);
}

class CuentaCorriente : Rendimientos {
  public Money GetInteres(Cuenta c) {
    return [calcular el interés de cuentas corrientes];
  }

class CuentaCheques : Rendimientos {
  public Money GetInteres(Cuenta c) {
    return [calcular el interés de cuentas de cheques];
  }

class CuentaSimple : Rendimientos {
  public Money GetInteres(Cuenta c) {
    return [calcular el interés de cuentas "comodity"];

public Money GetSaldo() {
  ...
  Saldo += rendimiento.GetInteres(this);

En este caso he optado por definir una interfaz llamada Rendimientos. Nótese que no he usado la nomenclatura usual agregando una "I" al principio, ya que si más adelante decido que hay código en común en las clases que la implementan, mismo que me gustaría consolidar, puedo convertir a Rendimientos en una clase abstracta sin demasiados problemas y sin preocuparme de tener ahora una nomenclatura inconsistente por un lado, o tener que cambiar todas las menciones de la misma por el otro.

Analicemos las ventajas y desventajas que tiene el enfoque alternativo:

  • Ventajas
    1. El código que hace uso del método polimórfico se simplifica considerablemente.
    2. Las clases y métodos resultantes de la descomposición son altamente cohesivos, virtud de lo cual
    3. Existe un lugar claro en el código para la definición de reglas de negocio relacionadas con las variantes identificadas.
    4. El código de los métodos es sencillo y por lo tanto más fácil de comprender.
    5. El código es más sencillo de probar, ya que solo maneja una variante a la vez.
  • Desventajas
    1. Es necesario crear un mayor número de clases.
    2. No se puede saber con solo observar el código cual método se ejecutará, ya que esto se resuelve en tiempo de ejecución.
    3. Requiere de un diseño más concienzudo que la inclusión ad hoc de sentencias IF "según se vaya ocupando".

Vuelvo a hacer énfasis en algo que he comentado varias veces. Algunas personas opinan que aparentemente, el uso de la programación orientada a objetos aumenta la complejidad del código. Esto es una falacia. Hay que distinguir entre "código simple" y "código simplón":

Código simple
Es código que expresa su intención de forma clara, aún si no se conocen todos los detalles que lo hacen funcionar.
Código simplón
Es código escrito con las mismas cinco instrucciones que aprendí en mi curso de Introducción a la Programación, sin importar cuan rebuscado pueda ser el resultado final.

Nadie se lo piensa dos veces al llamar ToString el su código o al concatenar una cadena con un número (usando implícitamente ToString). ¿Porqué entonces el crear mi propio código polimórfico es más complejo?

Teclear no es un sustituto para pensar.

Al descomponer esas largas y complejas rutinas en sub-rutinas, estamos aplicando "divide y vencerás". Al mover esas sub-rutinas a clases polimórficas, aplicamos "a cada quien lo suyo".

Con el tiempo, en lugar de tener unas pocas clases grandes y complejas, terminaremos con un montón de clases pequeñas y simples. De eso se trata la programación y muy en especial la programación orientada a objetos: De manejar la complejidad inherente de los sistemas. De ocultarla si es posible, no de incrementarla con programación descuidada y por debajo de lo que los usuarios esperan de nosotros.

Saludos.