Estructura de un proyecto con Clojure

Llevamos ya un tiempo hablando sobre Clojure, hemos visto como montar el entorno de desarrollo de Clojure, conocemos un poco de Clojure como lenguaje e incluso hemos usado Clojure para resolver un problema real.

El desarrollo de software no se reduce a escribir código, también hace falta aplicar técnicas más «ingenieriles» para gestionar dependencias, crear documentación, desarrollar pruebas automatizadas y preparar paquetes para distribuir nuestro software. En este post vamos a ver cómo estructurar un proyecto con Clojure para conseguir todo esto.

Leiningen es nuestro amigo

Leiningen es una herramienta que ya usamos en su momento para instalar Clojure y para hacer nuestros pinitos con el REPL y probar nuestros scripts, pero permite hacer muchas más cosas.

Con leiningen podemos gestionar varios aspetos del ciclo de vida del proyecto, desde la creación de un proyecto hasta la generación del paquete «distribuible» con la aplicación.

Para crear un proyecto con leiningen debemos ejecutar:

c:\dev>lein new app koalite-sample

Esto usará la plantilla de proyectos de aplicación (app) para generar un nuevo proyecto llamado koalite-sample dentro de la carpeta con el mismo nombre.

Con leiningen se pueden crear proyectos de distinto tipo, de forma similar a como con Visual Studio podemos crear aplicaciones web, de consola, de WPF, etc. Puedes ver los tipos de proyectos ejecutando lein help new.

Igual que hice en su momento en el tutorial de node.js y express, vamos a ver exactamente qué es lo que nos ha generado leiningen.

En la carpeta raíz del proyecto encontramos los típicos ficheros de todo repositorio de github, el .gitignore y README.md. Además, tenemos el fichero de descripción de proyecto, project.clj que contiene algo parecido a esto:

(defproject koalite-sample "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.4.0"]]
  :main koalite-sample.core)

Su misión es similar al packages.json de un proyecto de node.js, e incluye información sobre el proyecto en si mismo y sus dependencias.

Si queremos añadir dependencias adicionales, podemos añadirlas al vector de dependencias indicando su nombre y versión, y leiningen se encargará de descargarlas automáticamente por nosotros cuando vayamos a compilar o ejecutar la aplicación. Las depedendencias se almacenan de forma global en la carpeta %HOME%\.m2\repository, pudiendo coexistir distintas versiones de una misma librería sin ningún problema.

Además, se define el espacio de nombres que contendrá el punto de entrada de la aplicación, en este caso koalite-sample.core. Cuando se vaya a ejecutar al aplicación, se buscará la función -main (ojo al guión inicial) de ese espacio de nombres y se tomará como punto de entrada de la aplicación.

Es interesante notar una vez más la forma en que Clojure asimila código y datos. La información del proyecto son datos que típicamente estarían en un formato específico (XML en .NET, JSON en Javascript) y luego serían procesados por código externo. En este caso, para definir un proyecto se usa directamente código válido en Clojure a través de la macro defproject de leiningen (esto es similar a lo que hace rake en Ruby).

Al mismo nivel que project.clj encontramos las siguientes carpetas:

  • doc: esta carpeta contiene la documentación que queramos añadir al proyecto en forma de ficheros markdown.
  • src: contiene el código fuente del proyecto, organizado en subcarpetas que se corresponden con los espacios de nombres. Por defecto, incluirá una carpeta con el nombre del proyecto dentro de la cual encontraremos el fichero core.clj que contiene el esqueleto del programa de ejemplo, un típico hola mundo.
  • test: en una estructura paralela a la de la carpeta src, aquí encontraremos los ficheros con los tests unitarios.

Las distintas acciones que queramos realizar sobre el proyecto, se llevarán a cabo situándonos en la carpeta raíz y ejecutando comandos con leiningen.

Para ejecutar el proyecto, usaremos el comando run:

c:\dev\koalite-sample>lein run
Hello, World!

No es una aplicación muy espectacular, pero algo es algo. Si queremos jugar con ella, podemos lanzar el REPL desde la raíz:

c:\dev\koalite-sample>lein repl
...
koalite-sample.core=>(-main)
Hello, World!
nil

Es importante destacar que este REPL incluye nuestro código y todas las dependencias necesarias, que serán descargadas si no están presentes. Eso nos permite empezar rápidamente a probar la aplicación y sobrevivir sin un depurador.

Para lanzar los tests uniarios, debemos usar el comando test:

c:\dev\koalite-sample>lein test
Ran 1 tests containing 1 assertions.
0 failures, 0 errors.

Por último podemos empaquetar nuestra aplicación para distribuirla. Hay varias formas de empaquetado dependiendo del tipo de aplicación que sea y del tipo de usuario que vayamos a tener, pero lo más sencillo es utilizar el comando uberjar que genera un único fichero jar con la aplicación y todas sus dependencias:

c:\dev\koalite-sample>lein uberjar
Created c:\dev\koalite-sample\target\koalite-sample-0.1.0-SNAPSHOT.jar
Including koalite-sample.0.1.0-SNAPSHOT.jar
Including clojure-1.4.0.jar
...
Created c:\dev\koalite-sample\target\koalite-sample-0.1.0-SNAPSHOT-standalone.jar

En la carpeta target se generará un fichero sólo con nuestra aplicación, sin incluir Clojure ni dependencias llamado projectname-version.jar, y otro fichero que incluye nuestra aplicación y todas sus dependencias (incluyendo Clojure) llamado projectname-version-standalone.jar. Este último fichero podríamos llevarlo a cualquier máquina que tenga instalado el JRE de Java y ejecutarlo, sin necesidad de instalar nada más.

Resumen

Cuando no estoy trabajando con C# (o con Java), una de las cosas que más me gusta es poder olvidarme de entornos de desarrollo pesados como Visual Studio y Eclipse.

Trabajar con Clojure usando leinigen es muy fácil y tener una herramienta que se encarga a la vez de crear projectos, gestionar dependencias, ejecutar tests, compilar y empaquetar resulta muy cómodo. Tal vez la única pega es que en windows tarde mucho en arrancar, aunque eso más achacable a Java que a otra cosa (en linux es bastante más rápido).