Mostrando las entradas con la etiqueta smalltalk. Mostrar todas las entradas
Mostrando las entradas con la etiqueta smalltalk. Mostrar todas las entradas

04 abril 2012

Amber Smalltalk

Después de mucho tiempo volvi a ver el blog de James Robertson, donde me encontre con Amber Smalltalk: una implementación de Smalltalk hecha en JavaScript.

Seguramente la pregunta práctica es: ¿A quien le puede interesar interpretar Smalltalk con JavaScript?

Más alla de la respuesta obvia de: a los fanáticos de Smalltalk; hay algunos buenos casos donde Amber resulta interesante:

<modalidad-fanatico-smalltalk>
  • Enseñanza: Uno puede incrustar un classbrowser junto con una pagina tutorial, y usarlo para dar clases de programación orientada a objetos: los alumnos solo necesitan un browser.
  • Producción: Si existiesen las herramientas apropiadas, uno podría usar un entorno de objetos para desarrollar y luego exportar todo a un JavaScript minimizado y listo para usar con NodeJS o en un browser. (El gran obstáculo: las diferencias de sintaxis JavaScript/Smalltalk, JObjectProxy luce feo para quienes están acostumbrados al encadenamiento de mensajes en jQuery o el syntax sugar de CoffeScript).
</modalidad-fanatico-smalltalk>

De vuelta en mi modalidad realista.

Por ahora es un lindo experimento para ver, que demuestra una vez más como JavaScript se esta convirtiendo en el "assembler" de la web. 

Hay dos características de Smalltalk que lo convierten en un lenguaje unico, y son también su maldición:
  1. El "ambiente de objetos". Para que cualquiera que haya trabajado un tiempo considerable con Smalltalk volver a los archivos y las herramientas de build es como volver a la pre-historia.
    Sin embargo es una maldición por que aparta al lenguaje de todo el resto del mundo: usar herramientas existentes de versionado o editores comunes se vuelve doloroso y poco práctico.
  2. La sintaxis de message keywords es una camino de ida :)  y es una de las cosas que más extraño en otros lenguajes orientados a objetos: realmente ayuda a pensar en términos de objeto/mensaje.
    El lado negativo es que la diferencia de sintaxis complica la interoperabilidad con lenguajes tipo C como JavaScript.
    Creo que en este sentido los smalltalkers fueron muy conservadores: la sintaxis de Smalltalk es uniforme y genera cierto orgullo. Orgullo que debería dejarse de lado: las características menos elegantes de Groovy o Ruby resultan mucho más prácticas.
Mientras tanto voy observando la evolución de Dart, quien sabe quizas con sus snapshots logre lo que Smalltalk no pudo: lograr una tregua entre el ambiente de objetos y las herramientas basadas en archivos.

23 febrero 2011

Reingeniería de software (2da parte)

En el post anterior presente los términos utilizados comúnmente en reingeniería de software. En este voy a centrarme en una herramienta para design recovery -desarrollada por el Software Composition Group de la universidad de Berna- llamada Moose.

Moose esta compuesto de varios frameworks, de los cuales voy a mencionar los que creo principales para la tarea de design recovery:


  • FAMIX*: es un modelo que representa entidades de código y sus relaciones (para su posterior análisis estático). * En la documentación y papers que lei sobre Moose se me hizo un tanto confusa la diferencia entre FAME y FAMIX. Por lo que pude entender FAMIX es el nombre metamodelo y FAME la implementación en Moose para el intercambio de estos meta modelos.
  • Moose Core: trabaja sobre el meta modelo de FAMIX permitiendo hacer consultas usando Smalltalk.
  • Mondrian: es un framework de visualización, que facilita la creación de vistas polimetricas (vistas polimetricas? más a continuación).


Hasta acá todo muy lindo con la lista de herramientas, pero... ¿Cómo me ayudan a entender un sistema?

Cuando vi Moose por primera vez me hice la misma pregunta. En realidad, como no estaba familiarizado con las herramientas de design recovery mi pregunta fue mucho más básica: ¿Para que sirve Moose?

Si bien hay un libro online (estilo wiki), no me quedaba claro como se usaba la herramienta hasta que leí la tesis de doctorado de Michele Lanza (es especial el capitulo 4).
Para quienes quieran buscar ejemplos de como usar las herramientas de Moose, les recomiendo leer esta tesis. Y para los que no, les cuento brevemente de que trata.

Como mencione en el post anterior el problema principal de la reingeniería no es hacer el rediseño (o refactoring), si no entender lo que hay. Y como los sistemas suelen ser enormes, es necesario generar y visualizar de alguna manera información de resumen que ayude a entender las cosas a alto nivel, o al menos guiarnos hacia los lugares de importancia.

Con este objetivo Lanza trabajo en un tipo de visualización llamado vistas polimetricas que mezcla entidades de software y sus relaciones, junto con métricas de software. (en este articulo o en el libro Object-Oriented Metrics in Practice pueden encontrar una explicación detallada de las vistas polimetricas).

La idea de mezclar métricas en la visualización, es la de poder identificar visualmente posibles candidatos a mirar. Acá van algunos ejemplos:

System Hotspots View
Da una idea general del tamaño del sistema. En este gráfico cada rectángulo representa una clase, y su tamaño esta dado por el número de métodos. (Los cuadrados grises son meta clases, esto se hizo sobre un sistema en Smalltalk, en general tener muchos métodos en la meta clase es un indicador que están faltando abstracciones adecuadas -es casi como tener muchos métodos static en Java):



System Complexity View
Muestra las relaciones de herencia entre clases y los tamaños de cada una de ellas (ancho: número de atributos, alto: número de métodos).


Inheritance Classification View
Las dos vistas anteriores fueron un ejemplo de lo que Lanza llama "first contact" views, es decir sirven para tener un pantallazo muy general, en cambio esta vista se aplica a una jerarquía de clases.
La forma de árbol muestra relaciones de herencia, el ancho de las cajas la cantidad de métodos agregados, el alto la cantidad de métodos sobre-escritos y el color (en escala de grises) la cantidad de métodos sobre-escritos que llaman a super.


Estos fueron algunos ejemplos, hay más vistas utilizadas para obtener información a distintos niveles de detalle. Cabe aclarar que la idea es que estas herramientas de visualización sean interactivas (ej. pasar el mouse sobre cada caja nos dice la clase), de nada sirve tener cajas de colores si no sabemos de que parte del sistema nos esta hablando.

¿Donde entra Moose en todo esto? Moose es un framework que permite crear estas vistas y explotar la información. "Out of the box" no es una herramienta automática para hacer una evaluación del sistema, si no más bien un framework, que se puede automatizar (usando Smalltalk) e integrar a sistemas de integración continúa (como Hudson o mejor dicho Jenkins).
Nota: Moose esta hecho en Smalltalk, pero puede analizar sistemas en Java usando un conversor de Java al formato FAMIX.
¿Existen otras herramientas de este estilo?
Mas o menos. Moose es un framework general, lo más parecido a un framework general de este tipo es una vieja herramienta llamada Rigi (que ya parece estar obsoleta - al menos no se actualiza desde hace mucho).

Si existen sistemas que proveen visualizaciones particulares, por ejemplo CodeCity es un software muy interesante que lee modelos de FAMIX y realiza vistas basadas en métricas pero utilizando una metáfora de ciudad en 3d, por ejemplo la siguiente es una representación del JDK (versión 1.5) realizada con CodeCity
Si mal no recuerdo: alto=número de métodos, ancho=número de atributos, los colores varían según lo que se quiera ver por ejemplo se pueden utilizar para resaltar "design disharmonies" -otro término para "code smells". Las "manzanas" de la ciudad representan paquetes.




10 diciembre 2009

Test driven development... algunas lecciones aprendidas

En este post quería compartirles algunas cuestiones sobre Test Driven Development (TDD) que fui aprendiendo de los proyectos y personas con las que trabaje.

TDD != Testing en busca de bugs

Es muy común confundir TDD con testing: al menos comparten la palabra "test" en el nombre.

La diferencia esta en que uno generalmente usa la palabra "testing" para referirse a la búsqueda de bugs. Mientras que en TDD la intención es otra: uno busca agilizar el diseño, facilitando la incorporación de cambios.

A grandes rasgos TDD consiste en expresar con un programa las "expectativas" sobre lo que se va a desarrollar.


Supongamos, por ejemplo, que estamos aprendiendo C con libro de Kernighan y Ritchie y su ya famoso "Hello World", así que escribimos:

#include <stdio.h>
int main() {
    printf("Hello World\n")
    return 0;
}

Compilamos...

$ cc hello.c -o hello
hello.c: In function ‘main’:
hello.c:5: error: expected ‘;’ before ‘}’ token

Arreglamos código, compilamos y ejecutamos, hasta que vemos en la pantalla el resultado esperado:

Hello World

La diferencia usando TDD es que primero escribimos un programa para verificar el resultado esperado (algo que comúnmente hacemos de forma manual):

ejecutarPrograma();
verificar(salidaDePrograma, "Hello World");

Nuestro "test", no intenta buscar bugs probando diferentes casos, o casos "borde" que puedan ser problemáticos.
Si no que simplemente es una forma de establecer que es lo que queremos hacer.

Esta diferencia es sutil pero tiene muchas implicancias en la forma de trabajar:
  • No es necesario pensar casos de test complejos, solo basta con pensar que es lo que se quiere hacer.
  • Con TDD el ciclo de escribir código - compilar, se transforma en: escribir tests/código - compilar/ejecutar tests.
    Es decir, crear/ejecutar tests debe ser parte común del desarrollo. Si los tests tardan en ejecutarse se transforman en una carga.

En mi caso los "tests" usados de esta forma se convierten en una especie de "TODO list". En efecto muchas veces siento que si no tengo planteado un "test" (aunque sea en mi cabeza) no sé por donde empezar.

¿Vale la pena dedicar tiempo a escribir tests?


El ejemplo de "Hello World" muestra una cuestión evidente: programar el test puede ser mucho más difícil que hacer el programa a testear.
Cabe preguntarse si es siempre así, y si el trabajo adicional vale la pena.

En general no siempre es tan difícil escribir tests, y hay factores que influyen en la dificultad:
  • Componentes y frameworks "acoplados" a sistemas externos.
    Por ejemplo, si quisieran escribir el test del "Hello World" en C deberían capturar el STDOUT del programa. Suele ser fácil reemplazar la salida standard, aunque si el stream fuese parametrizable sería mucho más fácil.
  • Frameworks que no proveen interfaces separadas de a implementación.
    Siguiendo con el "Hello World", supongamos que podemos parametrizar el stream de salida, sería ideal poder capturar la salida en un String.
    Si el "Stream" que nos brinda el lenguaje no permite hacerlo, podemos hacer otra implementación que cumpla la misma interfaz y capture el String. Pero si "Stream" es una clase cerrada, estamos en problemas.
  • Diseños que no cumplen con el principio de Single Responsibility.
    Si la "responsabilidad" de lo que queremos testear es acotada, entonces el test también va a ser más corto y fácil de escribir.
  • Dependencias globales mutables (como Singletons que no son "singletons").
    En un post anterior ya hable sobre los problemas del estado global... así que voy a evitar la tentación de volver a comentarlos :)
  • Tests con grandes conjuntos de datos.
    Complican la mantención del test y por lo general también hacen que la ejecución sea más lenta.
  • El lenguaje/tecnología a usar influyen en la forma de trabajo.
    Por ejemplo en Smalltalk es muy común desarrollar mientras se ejecuta el test (completando lo que falta implementar en el debugger), ya que entorno permite modificar clases sin "parar el mundo". En Java este tipo de prácticas no es posible.
    En otro extremo el lenguaje C++ no cuenta con reflection out-of-the-box, y por lo tanto escribir tests es mucho más trabajoso (hay que indicarle al framework explicitamente que tests ejecutar).
    Es conveniente tener en cuenta la tecnología que se usa, por ejemplo en C++ intentería tener tests más grandes para evitar el "overhead" que implica crear tests nuevos. En Smalltalk, Java u otros lenguajes el entorno ayuda a que este problema no exista.

Desde un punto de vista de management, quizás la cuestión esta en cual es el "retorno de inversión" de lidiar con todo esto. Para mi el valor esta en que:

  • Al tener tests automatizados uno tiene más confianza a la hora de hacer refactorings o cambios en el diseño.
    Esto puedo afirmarlo con mi experiencia: trabaje en un sistema financiero donde teníamos una gran cantidad de tests (si no recuerdo mal, más de 10.000).
    Tuve que hacer un rediseño importante de una parte "core" del sistema, hice los cambios y emepecé a ver los tests que fallaban.
    Los tests que entendí, los arregle. Los que no, los consulte con sus autores y los arreglamos en conjunto. Todo este trabajo hubiese llevado muchísimo más tiempo sin tests automatizados.
  • Ayuda a tener un diseño más abierto a los cambios
    La razón es simple: cuanto más acoplado esta un objeto al uso de sistemas o frameworks externos, más difícil es testearlo. Por lo tanto con TDD uno tiende a diseñar de forma tal de disminuir este acoplamiento.
    Lo mismo ocurre cuando los objetos tienen dependencias globales mutables: se vuelven dificiles de testear por que un test cambia el estado global, haciendo que otro test falle. Por lo tanto uno tiende a evitar este tipo de globales.
    A la larga las consecuencias de usar TDD son beneficiosas para el diseño.

No todo es color verde...

Como mencione antes: la intención de TDD es facilitar cambios y ayudar en el diseño. No encontrar bugs.

Por lo tanto aunque la aplicación pase los tests de unidad, puede contener errores funcionales (casos de test mal planteados), bugs por casos "especiales" y problemas de UI.

Volviendo al ejemplo de este sistema en el que trabaje con más de 10.000 tests de unidad:
Al principio no teniamos un equipo de QA, los tests automáticos pasaban y algunos de nosotros probamos la aplicación durante el desarrollo.

Llego el día "D": empaquetamos todo para producción y llevamos el software al cliente.

A las horas recibimos un llamado: un botón de la UI permanecía deshabilitado y el cliente no podía acceder a una funcionalidad del programa (funcionalidad que estaba desarrollada, pero inaccesible). Asi que tuvimos que corregir el problema y hacer un nuevo deploy.

Los tests de unidad no reemplazan a un equipo de QA. No es la intención de TDD reemplazar las prácticas comunes de testing y verificación de calidad.

La diferencia esta en los detalles

Supongamos los convencí de las "bondades" de TDD. Asi que empiezan a practicarlo en un proyecto.

Al tiempo hay una alta probabilidad de que empiece a pasar lo siguiente. Algunos tests fallan pero arreglarlos es un problema: los tests se volvieron enormes e inmantenibles. Solución rápida: los tests que fallan se ignoran. Un par de meses despúes los tests que fallan se siguen ignorando, y se pierde la ventaja de TDD: si necesitamos hacer un cambio de diseño no tenemos forma de saber que rompimos.

¿Por que se llega a este punto?
Principalmente es una cuestión de la costumbre del equipo en hacer y mantener los tests.
Pero además hay un montón de detalles que con el tiempo se acumulan cual bola de nieve e influyen en que los tests se vuelvan inmantenibles. Algunos "detalles" a tener en cuenta:

No depender de bases de datos externas

Hacer que un test de unidad requiera de una base de datos es problemático por que:
  • El entorno de desarrollo es dificil de configurar: cada desarrollador tiene que configurar los drivers y conexiones para poder empezar a trabajar con los tests.
  • Si se usa una base compartida: un cambio hace que los tests fallen para el resto de los desarrolladores. Para hacer TDD los tests se deben correr todo el tiempo, asi que tener tests que fallen por cambios de otros es inadminsible.
  • Es más trabajoso para usar un servidor de integración continua: hay que asegurarse de que el servidor de integración use correctamente la base de datos de prueba.
  • La ejecución de los tests suele ser más lenta, asi que correr todos los tests no es algo que se haga muy seguido.

En el caso de que un test necesite una base de datos es necesario distinguir:
  • Que es lo que se esta testeando: ¿Quiero realmente testear el acceso a la base de datos?
  • ¿Puedo reemplazar el acceso a la base de datos, por una implementación que "simule" la respuesta y no acceda a una base de datos real?

Muchas veces uno quiere testear la implementación a la base de datos, por ejemplo se usa Hibernate para responder algunas consultas y cambiar la implementación por una que simule la base no aporta nada.
En esos casos lo mejor es usar una base en memoria (como HSQLDB) y levantarla durante el test.

HSQLDB es liviano, soporta Hibernate y toda la sintaxis de SQL. El unico punto en contra es que si uno quiere "ver" como se guardan las cosas en la base hay que frenar el test para no bajar el servidor.

El siguiente es un pequeño ejemplo de como usar HSQLDB en un test:

public class TestDatabase {
    public static final String DEFAULT_DATABASE_NAME = "test";
    public static final int DEFAULT_SERVER_PORT = 9001;

    private Server hsqldbServer;
    private HsqlProperties hsqldbProperties;
    private String jdbcConnectionUrl;
    
    public TestDatabase(String databaseName, int serverPort) {
        hsqldbProperties = new HsqlProperties();
        jdbcConnectionUrl = "jdbc:hsqldb:hsql://localhost:" + serverPort + "/" + databaseName;
        hsqldbProperties.setProperty("server.port", serverPort);
        hsqldbProperties.setProperty("server.database.0", databaseName);
        hsqldbProperties.setProperty("server.dbname.0", databaseName);
    }

    public static TestDatabase startWithDefaultConfiguration() {
        return new TestDatabase(DEFAULT_DATABASE_NAME, DEFAULT_SERVER_PORT).start();
    }

    private TestDatabase start() {
        hsqldbServer = new Server();
        hsqldbServer.setProperties(hsqldbProperties);
        hsqldbServer.start();
        return this;
    }

    public void shutdown() {
        if (hsqldbServer == null) return;
        hsqldbServer.shutdown();
    }
}

public class AccesoALaBaseTest {
    private static TestDatabase database;

    @BeforeClass
    public static void startTestDatabase() {
        database = TestDatabase.startWithDefaultConfiguration();
    }
    
    @AfterClass
    public static void shutdownHsqldbServer() {
        database.shutdown();
    }
}

No usar herencia para compartir instancias de prueba

Usar herencia para compartir instancias entre tests es un error muy común.

Supongamos que estamos testeando la implementación de CajaDeAhorro. Para crear una instancia de la caja de ahorros, necesitamos una instancia de Cliente, que a su vez necesita una instancia de Contrato, que a su vez necesita una instancia de... bueno se hacen a la idea: crear todas estas instancias es trabajoso, nos tomamos el trabajo :(

Despúes queremos testear CuentaCorriente, y queremos reusar las instancias que creamos para el test de CajaDeAhorro.
Muchas veces se suele crear una superclase para el test, supongamos AbstractTest, donde se colocan estas instancias que necesitamos compartir.
Los desarrolladores empiezan a heredar todos los tests de AbstractTest, y van "subiendo" instancias que quieren compartir entre tests.

¿Se ve el problema?
AbstractTest termina siendo una gran bolsa de gatos. Cuando se quiere refactorizar AbstractTest hay otro problema: es engorroso buscar las referencias a cada variable usada en las subclases. Por lo general este refactoring implica mucho trabajo y nadie lo hace. AbstractTest sobrevive a varias versiones transformandose en una inmensa bola de lodo.

La solución:
No usar herencia para compartir instancias entre los tests (usar herencia para re usar código es mala idea, en este caso es malisima).
Es preferible crear una clase, por ejemplo ClienteTestResource (suelo usar el sufijo "TestResource" para esas clases) que brinda instancias de Cliente para los tests.

Nada evita que ClienteTestResource no se convierta tambien en una bolsa de gatos, pero la cuestión es más controlada: uno puede crear clases distintas para agrupar recursos necesarios en los tests. Y a diferencia de AbstractTest, los desarrolladores solo usan ClienteTestResource cuando lo necesitan.

Evitar código redundante

Otro problema común es la actitud de: "hago copy & paste, total es un test!".

Esto es perjudicial: los tests tambien hay que mantenerlos. Si hay que hacer un "copy & paste" para crear instancias que se necesitan en el test... usar un "TestResource".

Si hay que implementar un método en común para facilitar el testing: ¿Entonces por que no implementarlo en el modelo? Por ejemplo, si estoy testeando el balance de una cuenta y el codigo del test es algo asi:

cuenta.balanceAl(crearFecha("01/01/2009"));
...

private function Date crearFecha(String s) {
      SimpleDateFormat sdf .....
      return sdf.parse(s);
}

Y empiezo a copiar "crearFecha" en varios tests, la alternativa es dar una interfaz amigable en Cuenta (la otra alternativa es tener una interfaz más amigable para construir instancias de Date en general, un buen ejemplo de esto en Smalltalk es el framework Chaltén, que pemite expresar fechas como: "Jaunary first, 2009" donde Jaunary es un objeto first y , son mensajes... asi que la expresión es basicamente da una fecha) que permita escribir:

cuenta.balanceAl("01/01/2009");

Eso no quita que el metodo balanceAl(Date) no tenga que existir. El formateo de fechas depende del Locale y no debería usarse con un Locale implicito en la aplicación. Sin embargo es conveniente pensar en la facilidad de uso de las interfaces que uno provee: las interfaces de los objetos son como las interfaces graficas.

Tener interfaces que permitan expresar las cosas de forma natural, usando defaults apropiados y simplificando ciertas cuestiones, ayuda muchisimo a la hora de escribir tests.

La clave es pensar que cuando uno programa esta construyendo un lenguaje, y esto abarca tambien a los tests. No es lo mismo escribir:

assertThat(coleccion, hasItem("hola"));

Que escribir:

boolean found = false;
for (String s : coleccion) {
    if (s.equals("hola")) found = true;
}
assertTrue(found);

(Nota: sé que existe coleccion.contains("hola") pero quería explicitar la fealdad de esta alternativa ;) )

Un test por "observación"

Supongamos que para una CajaDeAhorro queremos testear que:
  • Los depositos incrementan el balance.
  • Las extracciones decrementan el balance.
  • No se puede hacer una extracción si no hay fondos.

Uno puede estar tentado a testear todo esto junto:

@Test public void depositoYExtraccion() {
assertThat(cuenta.balance(), is(monto));
cuenta.depositar(monto);
assertThat(cuenta.balance(), is(monto * 2));
cuenta.extraer(cuenta.balance());
assertThat(cuenta.balance(), is(0));
try {
    cuenta.extraer(monto);
    fail();
} catch (NoHayFondosException e) {}
}

Pero tiene algunos problemas:
  • Si el test falla es necesario debuggear para saber si el problema esta en depositar, extraer o balance.
  • El ejemplo es chico, pero en tests más grandes los errores tienden ser más dificiles de ver.

En comparación esta solución es mejor:

@Test public void losDepositosIncrementanElBalance() {
    assertThat(cuenta.balance(), is(0));
    cuenta.depositar(monto);
    assertThat(cuenta.balance(), is(monto));
}
@Test public void lasExtraccionesDecrementanElBalance() {
    assertThat(cuenta.balance(), is(not(0)));
    cuenta.extraer(cuenta.balance());
    assertThat(cuenta.balance(), is(0));
}
@Test(expected=NoHayFondosException.class)
public void noSePuedeHacerUnaExtraccionSiNoHayFondos() {
    assertThat(cuenta.balance(), is(not(0)));
    cuenta.extraer(cuenta.balance() * 2);
}

Parece más largo pero tiene varias ventajas:
  • Es más facil ver que esta fallando.
  • Si un test falla es más facil de corregir.
  • Tiene (para mi) una implicación psicologica: ir haciendo pequeños tests que van pasando, en general me ayuda a tener un mejor ritmo de trabajo. Incluso entre pequeños test a veces se me ocurren rediseños que no habia pensado originalmente.

Evitar tests no deterministicos

Cuando el resultado esperado depende de un valor del "entorno", por ejemplo: la hora actual o un número random. Es común terminar con tests no deterministicos: a veces pasan y otras veces no.

Por ejemplo supongamos que estamos testeando una aplicación que genera registros de auditoria con la hora actual:

registroEsperado = new RegistroAuditoria(nuevoTimestamp, etc, etc, etc);
sistema.ejecutarMetodoQueGeneraAuditoria();
assertThat(sistema.logDeAuditoria(), hasItem(registroEsperado));

El problema es que si la comparación de RegistroAuditoria involucra comparar el "time stamp", entonces el test pasa o falla según la resolución de la hora y si hubo pausas en el medio de la ejecución (por ejemplo se ejecutó el GC).

Una forma de evitar este tipo de cosas es "fijar" estos valores. Por ejemplo podemos tener una interfaz Clock que provee la hora:

Clock fixedClock = new FixedClock(nuevoTimestamp);
sistema = new Sistema(fixedClock);
registroEsperado = new RegistroAuditoria(nuevoTimestamp, etc, etc, etc);
sistema.ejecutarMetodoQueGeneraAuditoria();
assertThat(sistema.logDeAuditoria(), hasItem(registroEsperado));

Ya no hay más problemas de indeterminismo :)

Evitar delays

Muchas veces por razones de concurrencia, se termina agregando al test un "delay" (por ejemplo se ejecuta un thread y quiero esperar a que el thread actualize un valor que despues voy a verificar).

Este tipo de test tiene dos problemas:
  • El delay hace que los tests corran más lento.
  • Por lo general resultan en tests no-deterministicos por que dependen de como la plataforma maneja la ejecución de los threads.

La alternativa es investigar un poco más lo que se esta testeando. Si es necesario tener el thread separado en el test conviene usar un semanforo (o monitor en Java) para hacer un wait hasta que se modifique el valor.

Lamentablemente no tengo ningun ejemplo a mano para mostrar en el blog.
Lo he hecho tanto en Smalltalk y en Java, y quizas lo unico que puedo agregar es que las cosas concurrentes son extremandamente dificiles de testear.
Algunas veces una solución intermedia es usar "yield" para que la VM ejecute otros procesos, esto salva el delay pero puede generar tambien tests no-deterministicos... la mejor opción sigue siendo usar algun tipo de semaforo.

De todas formas vale la pena hacer la "investigación" para evitar el delay: uno termina aprendiendo muchas cosas de concurrencia :), y tener un delay de un 1seg en varios tests es muy molesto.

Usar mock objects con cuidado

Un mock object es simplemente una implementación "de mentira" que se usa para reemplazar en un test a una implementación real (esta es al menos mi definición de mock object).

En el ejemplo anterior FixedClock es una implementación de mentira de Clock, que nos facilita el testing.

Existen frameworks de mock objects que por lo general:
  • Facilitan la implementación de mock objects usando meta-programación.
  • Permiten hacer tests de caja blanca, pudiendo validar si se ejecuto o no un método del mock object.

En mi experiecia estos frameworks terminan derivando en tests dificiles de mantener:
  • Los tests de "caja blanca" suelen ser una mala idea, al primer refactoring fallan y se convierten en un "dolor" de mantener. En esos casos es mejor re-plantearse el caso de test (y recordar cual es la intención de los tests en TDD).
  • En general es mucho más facil tener una interfaz y hacer la implementación de mentira para el test que usar un framework de mock objects.
  • Cuando no se generan dependencias adicionales (explicación a continuación) y no hay problemas como en el ejemplo de FixedClock, es más facil usar objetos reales que mock objects (usar un "TestResource" puede ayudar a escribir menos codigo en estos casos).
  • En lenguajes dinamicos como Smalltalk es muchisimo mas facil crear mock objects, pero tambien tienen problemas: si se usan muchos mock objects y se hace un refactoring, es probable que los test compilen y pasen... aunque deberían haber fallado.

Un pequeño hint: usar mock objects no es necesariamente malo, pero necesitar de mucha logica en un mock object es signo de que algo esta mal. A veces no se necesita un mock object si no una implementación más sencilla de la "interfaz" que queremos simular.

Ser cuidadoso con las dependencias de los tests

Ya estoy llegando al final de este enorme post.

El tema de las dependencias de tests requiere un poco más de explicación, asi que voy a ser breve:

Muchas veces uno expresa los tests a más alto nivel por ejemplo:

"El usuario agrega items al carrito de compras. Procede al checkout, donde obtiene una factura de los items que compró"

Este test abarca toda una historia de usuario y a veces es bueno automatizarlo.

Sin embargo hay que tener en cuenta que este tipo de test no solo es más largo, si no que posiblemente dependa de otras implementaciones: por ejemplo el carrito de compras se implementa en un proyecto, la generación de factura en otro y quien usa ambas implementaciones (llamemosle Cajero) usa interfaces asi que el proyecto donde esta el Cajero no depende de una implementacion particular de carrito de compra o el generador de factura.

¿Donde colocamos el test?
Si lo hacemos en el proyecto donde esta Cajero, y no usamos mock objects, generamos dependencias adicionales que no son necesarias (o quizas dependencias circulares que son problematicas). Y si usamos mock objects para todo, el test se vuelve más dificil de mantener.

Lo mejor en estos casos es ver el test a otro nivel. Si quieren pueden llamarle "test de integración" (siguiendo una convención que usabamos en Mercap, prefiero llamarles "user story test" por que abarcan una historia de usuario, y reservar el nombre "test de integración" para tests que verifican la integración con sistemas externos).

Es conveniente que este tipo de test este en un proyecto separado. De esta forma se evitan dependencias circulares, y es más facil compartir información entre dintintos "user story tests" (usando de "TestResources").

Además este tipo de test suele ser más lento, y por lo general no se ejecuta con la misma frecuencia que los tests de unidad.

El problema de este tipo de test es que son más grandes, tienen más dependencias y por lo tanto más dificiles de mantener.

Pero cuando las fechas de entrega apremian, a veces no hay tiempo para estar haciendo muchos tests a nivel unitario. En esos casos con tests de este estilo se puede testear la implementación manteniendo un estilo TDD (uno escribe el "user story test" antes de empezar), sin necesidad de crear un test por cada una de las clases que se usan "internamente". En este caso las herramientas de covertura pueden ayudar a ver que se cubre con el "user story test" y que no.

Si bien esta es una alternativa que complementa a los tests de unidad, no los reemplaza: los tests de unidad son una buena forma de trabajar a "nivel micro", pueden ejecutarse todo el tiempo durante el desarrollo y son más faciles de mantener.

Espero no haberlos dormido con este post enorme, trate de resumir un poco las "lecciones aprendidas" con TDD.
Hasta la proxima :)

13 julio 2009

Estados

Cuando encuentro código del siguiente estilo:

if (factura.getEstado() == EstadoFactura.PAGA) {
    // código
} else if (factura.getEstado() == EstadoFactura.ENTREGADA) {
    // código
} else {
    // otro
}

no puedo evitar pensar: "¡horrible!". Por que conozco los problemas que genera este tipo de código y sé que con otro diseño pueden evitarse.

La cuestión es explicar como mejorar el diseño. En estas situaciones, mis intentos de explicación son frases un tanto pedantes como: "este uso del 'if' es feo, por que las decisiones sobre que hacer según un 'estado', en objetos se pueden resolver utilizando polimorfismo".

Lamentablemente este esbozo de explicación no ayuda, y noto que en general produce la siguiente reacción: 
"Todo muy lindo esto de objetos, mensajes y polimorfismo. Pero tengo en mi tabla FACTURA un campo ESTADO y esto refleja directamente eso. Además ¿Cual es la solución? ¿Usar el patrón State? ¿Hacer un montón de clases para algo que resuelvo en un 'if'? Dejemos la 'estética de objetos' para la teoría." 

Una explicación técnica...

Voy a comenzar por el camino común de dejar en claro por que en objetos este tipo de "if" puede evitarse.

El siguiente ejemplo:

if (figura.getTipo() == Figura.RECTANGULO) {
    area = figura.getBase() * figura.getAltura();
} else if (figura.getTipo() == Figura.TRIANGULO) {
    area = figura.getBase() * figura.getAltura() / 2;
} else if (figura .... ya se hacen a la idea de como sigue

puede resolverse mediante clases que representen a los rectángulos, triángulos, etc. 
Si todas estas clases son polimorficas con el mensaje "getArea()" entonces toda la seguidilla de "if" puede resolverse en una sola línea: figura.getArea().
Donde el receptor del mensaje es quien encapsula la decisión de como hacer las cosas.


Complicando el problema

Muchas veces es deseable que el algoritmo a ejecutar según el tipo de objeto este separado de los objetos.
Por ejemplo si Figura posee un método "dibujar" es probable que el algoritmo de dibujo genere dependencias con un framework de dibujo, y quizás no quiero "atar" al modulo de figuras con un framework de dibujo en particular, o quizás el algoritmo de dibujo varíe según el contexto.

Para este tipo de casos las alternativas son usar Double Dispatch o bien algún tipo de interfaz entre distintos frameworks/implementaciones donde en este caso dibujar seria algo así como "dibujarSobre(unCanvas)" siendo "unCanvas" es un objeto que implementa esa interfaz común.

Pero volviendo al problema original (de la Factura y Estado), hay algunas cuestiones a tener en cuenta:
  1. En el caso de la factura el "if" se hace en base al estado y no a un "tipo de factura", eso significa que a diferencia del ejemplo de las figuras el estado puede variar una vez creada la factura.
  2. Crear un objeto que represente el estado y usar Double Dispatch no parece ser en este caso una buena solución. (El por que lo dejo como ejercicio)

¿Cuál es la solución a este problema?
Rta: Examinar mejor el dominio:

  • ¿Tiene sentido hablar de "estado" de una factura?
  • ¿Que representa la factura?
  • ¿Realmente el cambio de "estado" representa el cambio de estado de una misma cosa o en realidad representa cosas distintas?

Examinando el dominio

NOTA: El ejemplo de Factura/Estado es ficticio, pero creo que captura muchos de los casos de negocio donde encontré código del estilo "if estado then ...".

En "Stan's Shop" la gente agrega productos a su pedido, paga en la caja donde se le entrega una factura, finalmente en el mostrador de entregas un empleado prepara los productos y una vez que se los dio al cliente coloca un lindo sello de "ENTREGADO" en la factura (un esquema similar al que siguen todos los locales de comida rápida del micro-centro).

Los diseñadores del sistema pensaron que era bueno tener un objeto "factura" especificado por la siguiente clase:


Este diseño tiene varios problemas:
  • Si la factura se ve como un comprobante de pago (al momento de pensar este ejemplo ficticio desconozco si en el negocio tiene otros usos), entonces la factura debe ser inmutable. Este hecho se refleja en que agregarItem y quitarItem generan error: solución problemática por que en todos los lugares donde quiera enviar estos mensajes tengo que tener en cuenta la posibilidad de error. Lo mismo ocurre con getNumero donde se debe chequear por null.
  • La "mutabilidad" de factura genera otros problemas de implementación, pero prefiero dejar los detalles para otro post sobre "side effects".
  • ¿Cómo sé cuando una factura es "valida"? Puedo chequear por getEstado() == PAGA o ENTREGADA, puedo chequear por getNumero() != null o agregar un nuevo método esValida (de intention revealing ni hablar). En la práctica encontré que se mezclan las tres formas dependiendo del programador, lo que genera algunos dolores de cabeza al momento de hacer un refactoring.
  • ¿Tiene sentido tener un estado "ENTREGADA"? ¿El hecho de saber si los productos fueron entregados o no, no es responsabilidad de otra área de negocio? Este es un ejemplo ficticio y no aporta mucho escarbar en detalles de negocio, simplemente lo menciono por que en la mayoría de los casos los problemas de diseño muestran una falla en reconocer objetos del dominio.

La solución a este problema es sencilla:
  • La factura representa para mi un comprobante de pago y nada más. Por lo tanto una vez generada no puede modificarse ya que tiene implicaciones contables (y quizás legales).
  • Cuando el cliente llega al local y elige los productos que quiere no esta trabajando sobre una "factura", si no sobre una especie de "carrito de compras".
  • Saber si se entregaron o no los productos no es responsabilidad de la factura, si no del sistema que lleva en control de las entregas.



Con este diseño la separación de responsabilidades es clara, no hay forma que en el código modifique items en una factura, o de pedirle el número de factura a un CarritoDeCompras, por lo tanto no hay necesidad de verificar un "estado".
El caso de "ENTREGADA" lo voy a discutir a continuación por que me sirve para ejemplificar otro "patrón" común.

Cambios de estado sin cambios de comportamiento

En el ejemplo el cambio de CarritoDeCompras a Factura implica un cambio en la semantica de los objetos. ¿Pero que pasa con el sello de "ENTREGADA"? ¿No se puede pensar como un simple flag booleano en una Factura?

No quiero entrar en detalles de este dominio ficticio, pero quizás un "flag" no alcance. Es probable que quiera registrar quien realizo la entrega, a que hora, etc.
Por eso voy a hacer una simplificación: no me interesan esos detalles, solo quiero el equivalente al sello de "ENTREGADA".

Y dado que en este ejemplo yo pongo las reglas, voy a suponer que el operador en el mostrador de entregas tiene una pantalla que muestra las facturas pendientes de entrega, donde con un click puede cambiar el estado a "ENTREGADA".

Una forma de diseñar esto es pensar que uno tiene un "sistema" (modulo, o como deseen nombrar) que lleva el control de los pedidos, con dos "recipientes": uno para la facturas pedientes y otro para las entregadas. Entonces pasar de estado es mover la Factura de un recipiente a otro:





¿Pero el booleano no era mejor? No, este esquema tiene muchas ventajas más:
  • Factura sigue siendo inmutable, e independiente de como llevo el control de entregas.
  • SistemaDePedidos puede variar fácilmente, llevando por ejemplo el control de la fecha de entrega, etc.

¿Y la UI? "Quisiera hacer una página web que muestre el listado de facturas entregadas y no entregadas, con getEstado() o el flag es mucho más fácil".

En este caso también es fácil por que puede resolverse de la siguiente manera:
  1. La UI puede pedir al SistemaDePedidos directamente las Facturas entregadas o las no entregadas.
  2. Supongamos que la solución 1. no es satisfactoria: por como se utiliza la UI realmente se quiere tener un objeto al cual se le pueda pedir el estado. En ese caso conviene hacer un objeto especifico para la UI, este objeto puede verse como un DTO que se usa solo para transferir datos a la capa de UI.

¿Y la base de datos?
"Tengo una tabla Factura que tiene la columna ENTREGADA"

No hay problema: las herramientas de mapeo O/R permiten hacer mapeos complejos de asociaciones para resolver el caso entregada/pendiente en el SistemaDePedidos, o si es necesario discriminar la subclase a mapear.

Conclusiones

En el ejemplo ficticio que di en este post aplica a muchos dominios, algunos ejemplos que me vienen a la mente son: estados de un documento (publicado/no publicado) en un sistema de CMS, registración de compras, distinción entre un estado de "edición/en uso" para una configuración (ejercicio: examinen el código fuente de Struts y vean como la implementación de configuración puede refactorizarse y mejorarse usando lo que comente en este articulo), etc.

Creo que en la balanza de "bueno/malo" el código del estilo "if estado then codigo" tiene muchos problemas:
  • Probablemente no modela adecuadamente el negocio.
  • El "contrato" de los objetos es complicado: todo el tiempo dependo del estado para saber si puedo o no usar ciertos métodos.
  • En consecuencia el código se empieza a "contaminar" con este tipo de chequeos, con el agravante que quizás cada programador lo haga de una forma distinta según la situación, dificultando el mantenimiento y refactoring.
  • Se necesitan objetos mutables, en casos donde no conviene que lo sean (prometo hablar sobre "side effects" en otro post).

Mientras que las únicas ventajas que le veo son que es simple de implementar para ejemplos chicos y fácil de mapear a una tabla en la base de datos.

Por todas estas razones conviene evitar este tipo de "ifs" y en lo posible evitar el uso de Enums ya que fomentan este tipo de código.


04 febrero 2008

JUnit 4.4 y Hamcrest

A partir de la versión 4.4 de JUnit se incorporo al framework un cambio que permite expresar los assertions de una forma más natural. Tomando algunos ejemplos del release notes:

assertThat(x, is(3));
assertThat(x, is(not(4)));
assertThat(responseString, either(containsString("color")).or(containsString("colour")));
assertThat(myList, hasItem("3"));

Este cambio me pareció más que interesante. Primero por que la consecuencia de modelar ciertos conceptos muy abstractos como el chequeo realizado en un assert (llamado Matching en el framework) se obtuvo una interfaz con más "intention revealing" e información. Por ejemplo este tipo de assert puede mostrar mejores mensajes de error:

assertThat(responseString, anyOf(containsString("color"), containsString("colour")));
// ==> failure message:
// java.lang.AssertionError:
// Expected: (a string containing "color" or a string containing "colour")
//      got: "Please choose a font"

La otra consecuencia es que permite expresar más facilmente ciertos asserts que antes eran complicados, por ejemplo para chequear si una colección tiene ciertos elementos puede usarse directamente hasItems donde antes habría que crear una colección a parte y chequear por la inclusión de los elementos (o hacer un assert por elemento).

La buena noticia es que estos asserts especiales pueden utilizarse en versiones más viejas de JUnit ya que fueron "robados" de un proyecto llamado Hamcrest (ver tutorial).

Llevando la misma idea a Smalltalk

Si bien muchas expresiones son más simples en Smalltalk (debido al tipado dinamico y a los keyword messages) algo de este estilo:

assertThat(x, is(not(4)));

No queda de una forma linda:

self assertThat: x matches: (self is: (self not: 4))

Al principio pensé que aprovechando que en Smalltalk se puede "capturar" un DNU, podrían obtenerse expresiones similares. Por ejemplo al capturarse los DNUs para los selectors #assertThat:xxx:yyy:, determinar que Matcher utilizar en base a la porción "xxx:yyy:" del selector, luego el código quedaría asi:

self assertThat: x isNot: 4

Pero como podrán ver en el ejemplo en este caso el isNot no es una composición de Matchers, ouch!. (de todas formas estimo que is(not(4)) es equivalente a not(4)... si no la composición no me cierra, esta forma de hacer el "is not" debe ser solo por motivos de intention revealing, aunque no estoy con un Eclipse para poder comprobarlo).

La primer opción de crear los objetos directamente como en Java da un resultado que no queda tan expresivo, aunque es más simple y más flexible. 

Otra opción muy ala Smalltalk sería agregar metodos a Object para crear los Matchers (en lugar de usar la igualdad común y corriente), por ejemplo:

self assertThat: (x is: 4) negated

El problema de esta opción es que estos métodos para los Matchers tienen sentido solo en el contexto de los tests, por lo tanto agregarlos a Object sería una mala idea.

En fin, este tipo de "DSL Interno" como les llama Fowler (aunque yo prefiero llamarlos "truquitos sintacticos") tiene su particularidad en cada lenguaje. 

En Smalltalk las expresiones utilizando keyword messages y mensajes en cascadas son muy "lindas" para este tipo de cosas, pero en Java también los metodos static en conjunción con el "peligroso" import static, permiten usar Factory Methods para escribir expresiones "lindas".

La cuestión esta en si este tipo de assert agrega valor en Smalltalk. En Java yo creo que agrega mucho valor ya que la sintaxis del lenguaje hace dificil escribir cosas intention revealing y este me parece un lindo "hack" para lograrlas.

Otras caracteristicas de JUnit 4.4

Volviendo a JUnit, otra caracterisca robada de otro framework, es la capacidad de definir ciertos objetos que aplican a ciertos test (es decir definir explicitamente los "fixtures").

Por ejemplo es muy común escribir tests del estilo:

public void testResultWhenUserIsNotLoggedIn() // ....
public void testResultWithManagerUserSession() // ...
public void testResultWithGuestUserSession() // ...

No se si se capta la idea: cada test es para ciertos objetos (en este caso podrían ser sesiones de usuarios en diferentes roles).

Esta nueva caracteristica de JUnit, con el pomposo nombre de "Teorías", permite definir explicitamente los objetos y luego indicar mediante un "assumeThat" para caso aplica el test, seria algo asi:

@DataPoint private static Session nullSession = new NullSession();
public void testResultWhenUserIsNotLoggedIn(Session s) {
  assumeThat(session.notLoggedIn());
  // ....
}

Es decir los assumeThat actuan como predicados que indican que información corresponde al test.

Evidentemente esta caracteristica provee más información estatica sobre el test: un editor puede saber que "datos" se utilizan como entradas de los tests. 

Aunque no veo esta caracteristica con tanto entusiamo, me da la impresión que agrega mucho más complejidad al framework y no es tán facíl darse cuenta para que casos aplica cada test. Además por lo que vi en los ejemplos pareciera que las "Teorias" no están reificadas, eso genera que se usen anotations para cosas que se expresarian mejor de la manera "clasica".

Estos son mis "prejucios" sobre los Theories en JUnit 4.4... sería bueno ver comentarios de como esto funciona en un proyecto con muchos tests, o con "fixtures" complicados (los ejemplos en el sitio usan fixtures muy simples, la mayoría con tipos primitivos).

13 diciembre 2007

Smalltalks 2007

Ayer miércoles finalizaron las charlas de Smalltalks 2007.

Solo estuve presente en algunas charlas sobre Seaside y en el workshop de GLASS, y pese al calor de las aulas del Pabellón de la FCEyN, Smalltalks 2007 fue una sorpresa agradable.

 

Los organizadores hicieron un laburo bárbaro y eso se notó. (Felicitaciones!!)

 

Como comenté es un post anterior, hice el logo para estas conferencias -una vil copia del globo de la Byte con el obelisco de fondo ;)

El logo esta publicado bajo licencia de Creative Commons y lo pueden bajar de este link.

Conclusiones que saco de las charlas:

  • Hay gente haciendo cosas muy cool en Smalltalk: editores con refactorings para Traits o herramientas que combinan Smalltalk y Flex usando WebServices.
  • Seaside es excelente y espero que con el tiempo vayan surgiendo proveedores de hosting que lo soporten. Quizás a medida que se popularice la utilización de virtualización el hosting deje de ser un problema.
  • Percibo una actitud entre los Smalltalkers (no conozco el ambiente Ruby o Python) que no es muy copada: todo lo que esta en Smalltalk es bueno y el resto son estúpidos. Por ejemplo: En los comentarios finales de la charla sobre combinar Flex y Smalltalk surgió la pregunta lógica de "Por que tuviste que hacer la interfaz en Flex y no en St?", a lo que siguió la respuesta sensata de: "Flex me da buenas herramientas para UI y St para modelar el dominio, si hubiese tenido buenas herramientas de UI en St lo hubiese hecho en St". Acto seguido los comentarios en el publico fueron: "hay alguien trabajando en una librería de Cairo para VW" y "probablemente Sophie este en la próxima versión de Squeak".

    Esos comentarios si bien son muy positivos y entusiastas sobre Smalltalk, esconden mucho de miopía: Cairo es un API para dibujos 2d (como GDI+ o Java2D), Sophie es para crear libros multimedia y esta en una etapa alpha (basta bajarlo y probarlo un poco para darse cuenta) asi que actualmente estas no son ni remotamente alternativas a Flex.

    Sería bueno tener un buen framework/editor de UI en St, pero pensar que eso se hace en dos minutos es tonto, pese a que uno critique a Java o .Net como lenguajes, muchos de los frameworks y herramientas que tienen estas plataformas son muy difíciles de hacer.

    Asi que por que no emplear una actitud más post moderna y combinar St con otras herramientas? De esa manera uno se da el gusto de usar St y poder hacer las cosas.

09 octubre 2007

Dibujando el logo para el 1er. Congreso Argentino de Smalltalk

Hace unos días Hernán me pidió si podía hacer un logo para el congreso Argentino de Smalltalk que él esta organizando junto con Leandro Caniglia. Hoy acabo de terminar la primer versión del logo (no sé si será la versión final, todo depende de los comentarios de Hernán y Leandro). El dibujo no es nada original: es prácticamente una copia de la portada original de la Byte. El problema es que crear ilustraciones de este tipo que sean originales no es un trabajo menor: requiere de muchas pruebas con diferentes ideas, es decir requiere de un tiempo que no tengo :( Les comparto la primer versión junto con algunos tips para los que quieren aprender Illustrator: Tips para aprender Illustrator:
  • Es bueno aprenderse las combinaciones de tecla para cada una de las herramientas. La mayoría de las herramientas de la paleta se pueden seleccionar con una sola tecla, por ejemplo A - point selection, V - direct selection, P - path, etc. La ventaja: uno puede tener una mano en el teclado y la otra en lapiz/mouse. La diferencia entre laburar así y estar seleccionando las opciones de la paleta es enorme.
  • Teclas importantes: BARRA ESPACIADORA no importa que herramienta se este usando la barra muestra la mano y permite mover el canvas (esto es así en todas las aplicaciones de Adobe), Z - Zoom (yo uso mucho el zoom cuando estoy trabajando).
  • Aprender a utilizar: Live Paint, Clipping Masks y las herramientas para combinar figuras del path finder... ahorran mucho tiempo.
  • Me costo mucho darme cuenta como hacer un gradient transparente (odio la herramienta de gradient de Illustrator, es muy incomoda!), hasta que encontré esta pagina.
  • Uno puede aprender Illustrator de forma intuitiva... sin embargo es bueno ver algunos tutorials en la web, la UI de Illustrator tiene algunas cosas que no están a primera vista por ejemplo:
    • En la mayoría de los lugares donde se puede ingresar valores tipo 1px uno puede hacer cuentas en diferentes unidades: 1px+5pt.
    • La mayoría de las herramientas se comportan diferente cuando uno presiona Alt, Ctrl ó Shift (o la combinación de estas). Asi que vale la pena hacer la prueba
  • Sitios con tutorials:
    • PixelPerfect: Es un programa de TV vía web. La mayoría de los episodios son sobre Photoshop, pero hay algunos de Illustrator (muy recomendable)
    • Illustrator Techniques: tiene algunos tutorials on-line.

07 octubre 2007

El Smalltalk que no miramos

Usualmente los dialectos recomendados para empezar con Smalltalk son: Visual Works y Squeak, sin embargo rara vez alguien recomienda mirar GNU Smalltalk. ¿Por qué? Simplemente por que a primera vista GNU Smalltalk (gst) luce muy GNU y poco Smalltalk, es decir no hay un entorno grafico por default, si no herramietas de linea de comandos. Y por esa razón nunca tuve interés en mirarlo. Hace unos días anunciaron la puesta on-line de su nuevo sitio web (antes tenían una pagina muy escueta que tampoco motivaba demasiado). Mirando el sitio hubo un par de cosas que llamaron la atención e hicieron que me dieran ganas de ver este proyecto:
  • Soporte para embeber St en otras aplicaciones: esto es realmente importante por que fue lo que llevo a que lenguajes como Python o Lua se hagan conocidos (bueno Lua no es tan conocido...)
  • Soporte para continuations (lo que para envidia de algunos significa que en teoría es posible portar Seaside a este dialecto de St)
  • GUI toolkit con soporte para GTK: no me gusta GTK, pero que hayan logrado tener eso es importante, especialmente por que Squeak no tiene soporte para UIs nativas. (salvo por algún que otro proyecto que nunca se llegó a terminar).
  • Namespaces: mientras que en las listas de Squeak siguen discutiendo (1, 2, 3 y quizás más veces) como hacer la implementación perfecta de namespaces, gst ya los tiene (aunque no sean "perfectos"). En mi opinión esto aunque parezca una boludez es importante para poder compartir módulos y usar diferentes frameworks...
  • Ejecución de St desde la linea de comandos: es también otra cosa que parece boba y que en VisualWorks y Squeak se puede hacer con paquetes adicionales. Las diferencia: no hay que configurar imágenes especiales para eso y como gst es un paquete instalable en la mayoría de los linux basados en Debian, lo convierte en una buena alternativa como lenguaje de shell scripting.
Es por eso que mientras escribo esto estoy bajando Xubuntu para usarlo con VMWare y probar gst (desinstale la partición de Linux que tenia por que la mayoría de las veces uso la PC con programas que solo funcionan en windows). Creo que hay un paquete de gst para Cygwin... pero no me gusta Cygwin (para mi usar directamente un unix es mucho mejor que lidiar con el engendro mutante de cygwin). En fin en cuanto tenga tiempo de probar gst voy a actualizar el blog con algunos comentarios.