Java tiene una gran ventaja frente a los lenguajes y plataformas con los que trabajaba anteriormente: es portable. Eso significa que un PC y un Mac pueden ejecutar el mismo programa si disponen ambos de una máquina virtual adecuada. Sin embargo, Java también tiene su pequeño infierno en cuanto a compatibilidades se refiere: sus plataformas.
En un mundo ideal, no serian necesarias las distintas plataformas que se han creado, con todos su séquito de perfiles y configuraciones. Todos los dispositivos serían capaces de ejecutar una única plataforma con un único perfil. Desgraciadamente, debido a las diferentes configuraciones hardware, esto no es así y no nos queda más remedio que vivir con ello.
Sin embargo, hay veces que puede interesarnos crear un proyecto multiplataforma. Por ejemplo, podríamos desarrollar un juego muy interesante que se distribuyera tanto para ordenadores personales (J2SE) como para móviles (J2ME). Evidentemente la parte gráfica en ambas plataformas difiere y requiere una versión específica en cada caso, pero la lógica del juego probablemente sea idéntica en ambos casos.
Lo más común en estos casos es recurrir a crear una de las versiones (habitualmente la J2SE) y luego, con el código fuente obtenido, adaptarlo a la otra plataforma. Esto no solo es un incordio, sino que además ralentiza el ritmo de desarrollo y puede producir errores e incoherencias en el código fuente.
Por ello voy a presentar una forma sencilla de integrar las versiones de J2SE y J2ME o cualquiera de las plataformas y configuraciones disponibles. No se si alguien lo ha implementado antes, pero buscando información y preguntando en foros parece ser que nadie necesita o trabaja con proyectos compatibles J2SE/J2ME/J2EE.
Me centraré en un caso particular, el comentado del juego que requiere que se compile en J2ME y J2SE. Además supondremos que queremos sacar varios juegos con un mismo framework compatible para ambas plataformas.
Se supone que el juego va a tener una serie de clases comunes que deben funcionar en ambas plataformas y para todos los juegos (el núcleo). Dado que J2ME es un subconjunto de J2SE, crearemos una librería o un proyecto J2ME y pondremos todo el código del núcleo ahí. Obviamente dicho código no va a poder acceder a ciertas clases de J2SE por lo que el código será 100% compatible tanto con J2SE y J2ME.
Como queremos sacar juegos como churros, necesitaremos hacer que ciertas partes del framework no compatibles entre J2ME y J2SE como el interfaz gráfico se puedan instanciar desde distintos proyectos de juegos J2ME o J2SE (capa intermedia). Nada más fácil: creamos un par de proyectos, uno J2ME y otro J2SE con estas partes. Aquí viene el truco: incluiremos el proyecto del núcleo del framework desde aquí para tener acceso a todas sus funciones. Como dicho proyecto era J2ME compatible, servirá para ambas plataformas.
Por ultimo, tendremos que crear otros dos proyectos para generar el ejecutable final para J2ME y J2SE. En estos proyectos añadiremos el código básico del Midlet y el applet en cada caso e incluiremos (¡truco!) los dos proyectos anteriores respectivos: el núcleo y la capa intermedia correspondiente. La jerarquía queda como se ve en la figura.
A partir de este punto, cualquier cambio que se realice en el núcleo se propagará automáticamente al JAR final de todas las plataformas implicadas. El «truco» se puede usar incluso para compilar diferentes versiones compatibles con distintas configuraciones o perfiles. Lo he probado sólo en NetBeans, pero supongo que en Eclipse y otros IDEs será más o menos parecido el proceso.
Otro «truco» más
Si por ejemplo con una estructura como la mencionada quisiéramos tener acceso al sistema de ficheros, como en J2ME y J2SE no se utiliza la misma filosofía de acceso, tendríamos que crear clases diferenciadas para hacerlo. Pero como además seguramente querríamos disponer del acceso a disco desde el mismo núcleo, lo más recomendable es que creemos una clase abstracta «Fichero» y una factoría de ficheros «SistemaDeFicheros» en el núcleo. Cuando necesitemos un fichero simplemente accederemos a la factoría del núcleo y obtendremos un Fichero. Internamente la factoría del núcleo accederá a otra factoría situada en la capa intermedia. Para conseguirlo, se creará un interfaz para la factoría de la capa intermedia en el núcleo que se implementará en la capa intermedia. Durante la inicialización de la capa intermedia se pasará una factoría de la capa intermedia al núcleo permitiendo así que el núcleo cree instancias de «Fichero» propias de cada plataforma.
Si la clase «Fichero» no necesitara acceso desde el núcleo, este esquema se simplifica muchísimo. Simplemente crearíamos un interfaz «Fichero» en el núcleo y la implementaríamos en la capa intermedia. Dicha implementación estaría disponible tanto en la capa intermedia como en la aplicación o midlet final.
Conclusiones
Al no ser algo que en NetBeans se haya previsto, usar estas técnicas pueden producir algunos problemas menores. Por ejemplo, en mi proyecto de pruebas, al hacer un «clean & build» netbeans se queja de algunos ficheros tienen una fecha posterior a la actual. Simplemente con no hacerle caso está resuelto el problema, pero quizá se podría refinar un poco mas la técnica y evitar la aparición del warning.
Para mi ha sido un alivio no tener que repetir el trabajo, copiar y pegar, esperar a tener una versión estable para empezar con otra, etc. Sin duda la pequeña dosis de complejidad que se añade se ve completamente recompensada cuando ves tu aplicación funcionar en el PC y al momento pinchas en «deploy» y la ves funcionar también en el terminal. Te ahorras el trabajo mental de tener que desarrollar nuevas funcionalidades para una plataforma un día y esperar unas semanas a probar en la otra con el temor a que falle algo y no puedas continuar.
El ejemplo con el juego es bastante simple, pero imaginad el trabajo que puede ahorrar en una aplicación empresarial, donde además es posible que las aplicaciones J2ME y J2SE no sean ni siquiera idénticas. En cualquier caso es una elección muy personal.