19 de julio de 2011

Kata: Búsqueda Binaria

El propósito de la Kata es sencillo: practicar.

  • Los músicos practican escalas, arpegios, acordes, estudios, piezas completas, etc.

  • Los artistas marciales practican los movimientos individuales una y otra vez y después los combinan en secuencias de ataque-defensa-contra.

  • Los programadores... esperamos hasta la hora de la verdad... hmmm... tal vez no es la mejor estrategia.

El propósito del Kata no es buscar la solución a un problema. De hecho, la mayoría están basados en problemas simples, que han sido resueltos y documentados ampliamente en los libros de introducción a la ciencia de la computación.

Más bien, el valor de practicar un kata es practicar y afinar nuestras habilidades y disciplinas.

¿Cuantas veces nos hemos dicho a nosotros mismos que ahora si escribiré pruebas para el código de mi próximo proyecto?

¿O cuantas veces nos hemos prometido que ahora si vamos a aprender a usar correctamente los atajos de teclado del nuevo IDE que estamos comenzando a usar?

Dado que los kata son ejercicios controlados, sobre problemas cuya solución ya conocemos, nos dan la oportunidad perfecta para concentrarnos no tanto en la parte de descubrir o aprender el algoritmo, sino las partes mecánicas del proceso.

A continuación, procedo a desarrollar un kata muy básico: la búsqueda binaria.

Mi propósito para esta sección es hacerlo paso a paso siguiendo estrictamente el método de Desarrollo Dirigido por Pruebas (TDD por sus siglas en ingles)

El ritmo del TDD:

  • Fallo
  • Éxito
  • Refactorizar

También conocido como "Red, Green, Refactor!".

Las tres leyes del TDD:

  1. No se permite escribir nada de código de producción hasta no tener una prueba que falle (y no compilar es fallar).
  2. No se permite escribir una prueba más allá de lo necesario para que falle.
  3. No se permite escribir más código de producción que el necesario para pasar la prueba.

Escribimos nuestra primera prueba: El caso degenerado.

def test_bsearch():
   assert_equal(-1, bsearch(3, []))

if __name__ == '__main__':
   test_bsearch()

puesto que no existe la función bsearch, obtenemos un error:

Traceback (most recent call last):
 File "chop.py", line 64, in <module>
   test_bsearch()
 File "chop.py", line 61, in test_bsearch
   assert_equal(-1, bsearch(3, []))
NameError: global name 'bsearch' is not defined

A continuación hacemos lo mínimo necesario para corregir el error:

def bsearch(what, seq):
   pass

Ahora tenemos:

Traceback (most recent call last):
 File "chop.py", line 67, in <module>
   test_bsearch()
 File "chop.py", line 64, in test_bsearch
   assert_equal(-1, bsearch(3, []))
 File "chop.py", line 47, in assert_equal
   assert expected == actual, message
AssertionError: -1 was expected, but got None

Ahora hacemos lo más simple que pueda funcionar para lograr que pase la prueba:

def bsearch(what, seq):
   return -1

Con esto es bastante (si, algo bobo será, pero tengamos paciencia. Apenas estamos comenzando). Ahora escribamos la siguiente prueba:

def test_bsearch():
   assert_equal(-1, bsearch(3, []))
   assert_equal(-1, bsearch(3, [1]))

Pasa sin hacer nada. Sigamos:

def test_bsearch():
   assert_equal(-1, bsearch(3, []))
   assert_equal(-1, bsearch(3, [1]))
   assert_equal(0,  bsearch(1, [1]))

Falla. ¿Qué es lo más simple que podemos hacer para que pase, junto con las dos anteriores?

def bsearch(what, seq):
   if len(seq):
       if seq[0] == what:
           return 0
   return -1

La prueba pasa. Sigamos:

def test_bsearch():
   assert_equal(-1, bsearch(3, []))
   assert_equal(-1, bsearch(3, [1]))
   assert_equal(0,  bsearch(1, [1]))

   assert_equal(-1,  bsearch(0, [1, 3]))
   assert_equal(0,  bsearch(1, [1, 3]))
   assert_equal(1,  bsearch(3, [1, 3]))

Las pruebas 4 y 5 pasan sin modificación. La 6 falla, sin embargo.

def bsearch(what, seq):
   if len(seq):
       if seq[0] == what:
           return 0
       if seq[1] == what:
           return 1
   return -1

¡¡Woops!!

Traceback (most recent call last):
 File "chop.py", line 79, in <module>
   test_bsearch()
 File "chop.py", line 71, in test_bsearch
   assert_equal(-1, bsearch(3, [1]))
 File "chop.py", line 65, in bsearch
   if seq[1] == what:
IndexError: list index out of range

Claro, debemos tomar en consideración las dimensiones de la lista:

def bsearch(what, seq):
   start, end = 0, len(seq) - 1
   if start <= end:
       if seq[start] == what:
           return start
       elif seq[end] == what:
           return end
   return -1

Listo. Continuamos:

def test_bsearch():
   assert_equal(-1, bsearch(3, []))
   assert_equal(-1, bsearch(3, [1]))
   assert_equal(0,  bsearch(1, [1]))

   assert_equal(-1,  bsearch(0, [1, 3]))
   assert_equal(-1,  bsearch(2, [1, 3]))
   assert_equal(-1,  bsearch(4, [1, 3]))
   assert_equal(0,  bsearch(1, [1, 3]))
   assert_equal(1,  bsearch(3, [1, 3]))

   assert_equal(0,  bsearch(1, [1, 3, 5]))
   assert_equal(1,  bsearch(3, [1, 3, 5]))

Ahora el problema es que el elemento que buscamos se encuentra justo en medio de la lista. Podríamos hacer algo bobo como if seq[1] == what: return 1, pero eso no nos lleva a ningún lado. Recordemos que...

"Conforme las pruebas se vuelven más específicas, el código se vuelve más genérico."
def bsearch(what, seq):
   start, end = 0, len(seq) - 1

   if start <= end:
       half = (start + end) / 2
       if seq[half] == what:
           return half
       if seq[start] == what:
           return start
       if seq[end] == what:
           return end
   return -1

Siguiente prueba:

def test_bsearch():
   assert_equal(-1, bsearch(3, []))
   assert_equal(-1, bsearch(3, [1]))
   assert_equal(0,  bsearch(1, [1]))

   assert_equal(-1,  bsearch(0, [1, 3]))
   assert_equal(0,  bsearch(1, [1, 3]))
   assert_equal(1,  bsearch(3, [1, 3]))

   assert_equal(0,  bsearch(1, [1, 3, 5]))
   assert_equal(1,  bsearch(3, [1, 3, 5]))
   assert_equal(2,  bsearch(5, [1, 3, 5]))
   assert_equal(-1, bsearch(0, [1, 3, 5]))
   assert_equal(-1, bsearch(2, [1, 3, 5]))
   assert_equal(-1, bsearch(4, [1, 3, 5]))
   assert_equal(-1, bsearch(6, [1, 3, 5]))

Esto se está comenzando a poner algo redundante... tiempo de refactorizar:

def impares(cuantos):
   limite = 2 * cuantos
   return [indice for indice in xrange(1, limite, 2)]

def pares(cuantos):
   limite = 2 * (cuantos + 1)
   return [indice for indice in xrange(0, limite, 2)]

@trace
def bsearch(what, seq):
   start, end = 0, len(seq) - 1

   if start <= end:
       half = (start + end) / 2
       if seq[half] == what:
           return half
       if seq[start] == what:
           return start
       if seq[end] == what:
           return end
   return -1

def test_bsearch(limite):
   for cuantos in xrange(limite):
       lista = impares(cuantos)
          
       for indice, valor in enumerate(lista):
           assert_equal(indice, bsearch(valor, lista))

       for not_found in pares(cuantos):
           assert_equal(-1, bsearch(not_found, lista))

if __name__ == '__main__':
   test_bsearch(4)

Bien, hasta este punto todas las pruebas pasan:

$ python chop.py
bsearch(0, []) -> -1
bsearch(1, [1]) -> 0
bsearch(0, [1]) -> -1
bsearch(2, [1]) -> -1
bsearch(1, [1, 3]) -> 0
bsearch(3, [1, 3]) -> 1
bsearch(0, [1, 3]) -> -1
bsearch(2, [1, 3]) -> -1
bsearch(4, [1, 3]) -> -1

Siguiente prueba:

if __name__ == '__main__':
   test_bsearch(5)

Falla:

...
bsearch(1, [1, 3, 5, 7]) -> 0
bsearch(3, [1, 3, 5, 7]) -> 1
bsearch(5, [1, 3, 5, 7]) -> -1
AssertionError: 2 was expected, but got -1

Bien, ahora el resultado no está ni al principio, ni al final ni tampoco en el medio (recordemos que estamos usando aritmética entera, por lo que (0 + 3) / 2 = 1).

Eso significa que debemos examinar los elementos intermedios. Pero recordemos que si procedemos a evaluarlos uno por uno como lo hemos venido haciendo, la búsqueda se convierte en una operación con un tiempo de ejecución de O(n). Por ello, solo hemos de examinar la mitad con probabilidad de contener el elemento buscado:

def bsearch(what, seq):
   start, end = 0, len(seq) - 1

   while start <= end:
       half = (start + end) / 2
       if seq[half] == what:
           return half
       if seq[half] < what:
           start = half + 1
       else:
           end = half - 1
   return -1

Todas las pruebas pasan:

...
test_bsearch(14)
...
test_bsearch(19)
...
test_bsearch(25)

¿Qué pasa si en lugar de listas de números impares usaramos listas de números pares?

if __name__ == '__main__':
   test_bsearch(5)
   pares, impares = impares, pares
   test_bsearch(5)

Desarrollo

Como los músicos harían, una vez que tenemos una técnica bajo nuestro comando, habrá que buscar todas las variaciones posibles.

¿Podemos escribir una versión recursiva en lugar de iterativa?, ¿Usar multiples threads?, ¿En cuantos lenguajes podemos desarrollar nuestra kata, logrando que nuestra implementación no solo sea correcta, sino también idiomática del lenguaje seleccionado?

Saludos

Apéndice

A continuación, incluyo el código de las funciones trace y assert_equal, usadas en el desarrollo del kata.

class ArgFormatter(object):

   def __init__(self, args, kwds):
       (self.args, self.kwds) = (args, kwds)
       self.params = []

   def _clear(self):
       self.params = []

   def _addPositional(self):
       self.params.extend(repr(arg) for arg in self.args)

   def _addKeywords(self):
       self.params.extend('%s=%r' % (k, valor) for (k, valor) in self.kwds.iteritems())

   def _assemble(self):
       return ', '.join(self.params)

   def Format(self):
       self._clear()
       self._addPositional()
       self._addKeywords()
       return self._assemble()


def trace(f):
   from functools import wraps

   def formatParams(args, kwds):
       return ArgFormatter(args, kwds).Format()

   @wraps(f)
   def decorator(*args, **kwds):
       result = None
       params = formatParams(args, kwds)
       print '%s(%s) ->' % (f.__name__, params),
       result = f(*args, **kwds)
       print repr(result)
       return result
   return decorator


def assert_equal(expected, actual):
   message = "%s was expected, but got %s" % (expected, actual)
   assert expected == actual, message

28 de mayo de 2011

Interfaz de línea de comandos (CLI) y separación de ocupaciones.

Hace no mucho, me dí a la tarea de comenzar a crear una interfaz de línea de comandos (CLI por sus siglas en inglés) para el sistema al que actualmente estoy dando mantenimiento, además de ser un ejercicio intelectual bastante provechoso, obtuve resultados muy satisfactorios en cuanto al objetivo de lograr una separación del código de interfaz de usuario del código de dominio, el cual es uno de los problemas más críticos (y crónicos) que tiene el sistema en cuestión.


Como sabemos, debido a la filosofía subyacente en la gran mayoría de los entornos gráficos de programación actualmente en uso, los famosos IDE-RAD's (Rapid Application Development Environments), primero se diseña la interfaz de usuario y luego como no hay ningún lugar para poner la lógica del dominio, como no sea en la base de datos (stored procedures), si no se tiene cuidado, éste código acaba por desgracia casi siempre horriblemente mezclado y acoplado con el código de la interfaz de usuario. La base de código fuente así creada, se torna difícilmente mantenible y con el paso del tiempo y al ir cambiando los requerimientos se convierte en un serio problema en el mejor de los casos y la mayoría de las veces termina como una gran "bola de lodo" (del antipatrón big ball of mud).


Ya desde finales de los años 70's del siglo pasado, Smalltalk-80 introducía el paradigma (patrón de diseño propiamente dicho) MVC (Model-View-Controller) para evitar los problemas antes mencionados. Ya tendremos ocasión de discutir sobre el mencionado patrón de diseño posteriormente.


La cuestión es que muchos de nosotros no nos damos cuenta del problema, lo que es peor, ni siquiera lo vemos como tal. En mi caso fué hasta después de muchos años de recurrentes problemas de mantenimiento de código y de encontrarme una y otra vez sumergido en pantanos de código así mezclado (sin mencionar el sistema al que actualmente doy mantenimiento), que definitivamente supe que algo no estaba bien, mas aún: que algo estaba muy, muy mal.


Al principio no sabía exactamente que éra. Andando el tiempo y después de investigar, toparme con la verdadera POO y los patrones de diseño, sufrir un cambio de paradigma (paradigm-shift) y sobre todo, después de conocer, reflexionar e investigar mas a fondo algunos principios fundamentales de la programación tales como la modularidad, la no-redundancia, la claridad, la simplicidad, la alta cohesión, el bajo acoplamiento, la verificabilidad, entre otros, fué que me dí cuenta de la situación real.


Me dí cuenta que la razón, causa y origen de mis problemas era precisamente el estar transgrediendo sistemáticamente un principio fundamental que tiene su origen y aplicación en todas las areas de la vida en general y no solamente en la programación de sistemas informáticos. Me refiero al principio de separación de ocupaciones (y/o preocupaciones), mejor conocido en inglés como separation of concerns.


El origen del problema era que estaba yo mezclando a la fuerza y en un mismo recipiente dos cosas que son totalmente distintas e independientes una de la otra ("ortogonales" dirían algunos). Esto es: la interfaz de usuario y la lógica del dominio.


A continuación quiero compartir con ustedes algunas citas memorables de Martin Fowler sobre el tema.


"Domain objects should be completely self contained and work without reference to the presentation, they should also be able to support multiple presentations, possibly simultaneously. This approach was also an important part of the Unix culture, and continues today allowing many applications to be manipulated through both a graphical and command-line interface." - http://martinfowler.com/eaaDev/uiArchs.html


"So, if you write an application with a WIMP (windows, icons, mouse, and pointer) GUI, you should be able to write a command line interface that does everything that you can do through the WIMP interface—without copying any code from the WIMP into the command line." - http://www.martinfowler.com/ieeeSoftware/separation.pdf


"A good mental test to use to check you are using Separated Presentation is to imagine a completely different user interface. If you are writing a GUI imagine writing a command line interface for the same application. Ask yourself if anything would be duplicated between the GUI and command line presentation code - if it is then it's a good candidate for moving to the domain." - http://martinfowler.com/eaaDev/SeparatedPresentation.html


Así pues, al verme enfrentado por enésima ocasión al mismo problema del alto acoplamiento (o mezcla completa) de la interfaz de usuario con la lógica del dominio. Decidí llevar a la práctica los consejos de Martin Fowler y me puse a escribir una interfaz de línea de comandos para el módulo del sistema al cual estaba dando mantenimiento en ese momento. Al hacerlo obtuve una visión sumamente clara de los objetos de dominio necesarios así como de dónde y de qué manera necesitaba refactorizar el código. Asimismo, obtuve código mas limpio, objetos mejor factorizados, separación completa de la interfaz de usuario en el módulo en desarrollo y además una nueva forma de probar los objetos del dominio sin la interfaz de usuario. ¡Todo desde la línea de comandos!.


La experiencia anterior me ha llevado a recomendar ampliamente el ejercicio de escribir una interfaz de línea de comandos para las partes de los sistemas con interfaz de usuario gráfica (GUI) en los cuales no estemos muy seguros de cuáles sean los objetos de dominio necesarios o bien tengamos los problemas crónicos de acoplamiento anteriormente mencionados.


Esto también me hizo recordar que el proceso de creación de una aplicación GUI en Smalltalk es: 1.- Diseñar el modelo del dominio 2.- Probar el modelo del dominio 3.- Diseñar la interfaz de usuario. (http://www.object-arts.com/downloads/docs/index.html)


O dicho de otra forma: Primero lo primero (First things first). En cuenstiones de programación y en especial en POO: El dominio es primero.


Gracias por leernos, buen fin de semana y como siempre todos sus comentarios son bienvenidos.


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.