org.gvsig.tools.locator
Patrón Locator para librerías
El patrón Locator
El Locator es un objeto que proporciona referencias a componentes, servicios u objetos en general de una aplicación.
Este patrón permite separar cómo se gestiona o instancia un objeto, de la propia implementación del objeto. Además, esta independencia también aplica a los clientes del objeto, que no tienen que conocer cómo deben acceder o instanciar una referencia a un objeto, delegando esta tarea en el Locator.
De esta forma, podemos sustituir la opción de tener un Singleton por cada objeto de la aplicación o sistema, por un Locator. Aunque para la implementación de un Locator se suele emplear el patrón Singleton.
Dependiendo del tamaño y estructura de la aplicación, se suele tener un único Locator para toda la aplicación, uno por cada sistema o librería, etc.
Hay que destacar que el patrón Locator no es un patrón de creación, como los de tipo Factory, sino que sólo se centra en la obtención de referencias. La creación de las instancias que devolverá el Locator queda fuera del patrón.
Aplicación del Locator en gvSIG
Locator
Para gvSIG, el patrón del Locator se aplicará con una implementación basada en el uso de los puntos de extensión. Para los módulos o librerías, se usará además un mecanismo de inicialización que permita registrar las implementaciones que aporte en su Locator.
Para la implementación del Locator se ha creado un interfaz, que define los métodos de uso general, una implementación abstracta, basada en el uso de los puntos de extensión, y una clase base:
El interfaz del Locator se han definido métodos que permiten registrar clases, o factorías, así cómo obtener instancias de los mismos. Hay que tener en cuenta que el comportamiento del Locator no es como factoría, por lo que se espera que el método get devuelva cada vez la misma instancia de objeto. Es decir, actúa como un contenedor de referencias a objetos que funcionan como Singleton.
La implementación abstracta la aporta la clase AbstractLocator, y la implentación básica BaseLocator. La idea es que cada librería implemente su Locator extendiendo esta clase, aportando métodos que registren y obtengan referencias a sus tipos de objetos directamente. Más adelante, se muestra un ejemplo de implementación que aporta este tipo de métodos. Además, cada implementación de Locator deberá hacerse empleando el patrón Singleton, para facilitar el acceso al Locator.
El uso del Locator nos aporta algunas ventajas adicionales, como son:
- Las implementaciones no incorporan nada en lo que respecta a cómo se crean o se obtienen, lo que mejora su extensibilidad y la creación de tests unitarios. Además, se facilita la programación con interfaces y la definición de APIs.
- Soporte a implementaciones alternativas: como por ejemplo para Java SE y Java ME, o cuando haya una implementación básica, y el usuario pueda instalar una implementación extendida.
- Soporte de múltiples implementaciones: si existen distintas implementaciones para un mismo interfaz o componente, que se usan dependiendo de distintas opciones, como acciones del usuario, tipos de datos, etc.
Ejemplo de uso
A continuación se plantea un ejemplo, basado en la librería libCompat. Esta librería plantea el caso de implementaciones alternativas para Java SE y Java ME, por lo que requerirá una estructura de proyectos que permita organizar la librería en tres componentes de instalación:
- libCompat:
- Será la librería de API. Contendrá los APIs de la librería, en forma de interfaces de las distintas utilidades que aporta (StringUtils, FileUtils, etc.), así como un CompatLocator que dé acceso a las implementaciones correspondientes de dichas utilidades. También puede contener algunas implementaciones Base o Abstractas, que se empleen de forma común en las implementaciones por entorno.
- libCompatSE:
- Incluirá las implementaciones para Java SE.
- libCompatME:
- Proporciona las implementaciones para Java ME.
A continuación se muestran ejemplos de código, centrándonos en el uso de una de las utilidades de libCompat: StringUtils.
Primero, en libCompat se incluirá la definición del interfaz de StringUtils:
package org.gvsig.compat.lang;
public interface StringUtils {
String[] split(String input, String regex);
...
}
En libCompatSE, tendremos una implementación:
package org.gvsig.compat.se.lang;
import org.gvsig.compat.lang.StringUtils;
public class StandardStringUtils implements StringUtils {
public String[] split(String input, String regex) {
return input == null ? null : input.split(regex);
}
...
}
En libCompatME, la implementación alternativa:
package org.gvsig.compat.me.lang;
import java.util.regex.Pattern;
import org.gvsig.compat.lang.StringUtils;
public class MobileStringUtils implements StringUtils {
public String[] split(String input, String regex) {
return input == null ? null : Pattern.compile(regex).split(input, 0);
}
...
}
En libCompat tenemos la implementación del Locator de la librería, que quedaría como sigue:
package org.gvsig.compat;
import org.gvsig.compat.lang.StringUtils;
public class CompatLocator extends BaseLocator {
/** The name of the StringUtils reference. */
public static final String STRINGUTILS_NAME = "StringUtils";
/** Unique instance. */
private static final CompatLocator instance = new CompatLocator();
/**
* Return the singleton instance.
* @return the singleton instance
*/
public static CompatLocator getInstance() {
return instance;
}
/**
* Return a reference to StringUtils.
* @return a reference to StringUtils
* @throws LocatorException
* if there is no access to the class or the class cannot be
* instantiated
* @see Locator#get(String)
*/
public static StringUtils getStringUtils() throws LocatorException {
return (StringUtils) getInstance().get(STRINGUTILS_NAME);
}
/**
* Registers the Class implementing the StringUtils interface.
* @param clazz
* implementing the StringUtils interface
*/
public static void registerStringUtils(Class clazz) {
getInstance()
.register(STRINGUTILS_NAME, STRINGUTILS_DESCRIPTION, clazz);
}
}
En esta implementación de Locator, se han añadido métodos estáticos que facilitan el uso del mismo, y manejan referencias de los tipos de la librería en concreto, como el StringUtils. Así, por ejemplo, los clientes no tienen porqué usar el método get(name) genérico del Locator, sino que pueden usar un método getStringUtils() directamente.
Una clase cliente que quiera usar, por ejemplo, el StringUtils, podrá hacerlo de la siguiente forma:
public class SampleClient {
private StringUtils stringUtils = CompatLocator.getStringUtils();
public void doSomething() {
String text = "one,two,three";
String[] words = stringUtils.split(text, ",");
...
}
}
Sólo quedaría por ver cómo se registran las implementaciones a usar en el Locator. Para ello emplearemos el mecanismo de inicialización de librerias del paquete org.gvsig.tools.library .
libCompatSE y libCompatME tendrán cada una su Library que registre la implementación correspondiente de StringUtils.
SECompatLibrary:
package org.gvsig.compat.se;
import org.gvsig.compat.CompatLocator;
/**
* Initialization of the libCompat library, Java Standard Edition
* implementation.
* @author <a href="mailto:cordin@disid.com">Cèsar Ordiñana</a>
*/
public class SECompatLibrary extends BaseLibrary {
protected void doInitialize() {
super.initialize();
CompatLocator.registerStringUtils(StandardStringUtils.class);
}
protected void doPostInitialize() {
}
}
MECompatLibrary:
package org.gvsig.compat.me;
import org.gvsig.compat.CompatLocator;
/**
* Initialization of the libCompat library, Java Mobile Edition implementation.
* @author <a href="mailto:cordin@disid.com">Cèsar Ordiñana</a>
*/
public class MECompatLibrary extends BaseLibrary {
protected void doInitialize() {
super.initialize();
CompatLocator.registerStringUtils(MobileStringUtils.class);
}
protected void doPostInitialize() {
}
}
La librería de API libCompat, tiene un Library que comprueba si se ha registrado alguna de las implementaciones, lo que permite validar rápidamente si ha habido algun problema de configuración y no se ha registrado ninguna implementación para StringUtils:
CompatLibrary:
package org.gvsig.compat;
import org.gvsig.compat.lang.StringUtils;
/**
* Initialization of the libCompat library.
* @author <a href="mailto:cordin@disid.com">Cèsar Ordiñana</a>
*/
public class CompatLibrary extends BaseLibrary {
protected void doInitialize() {
}
protected void doPostInitialize() {
super.postInitialize();
// Validate there is any implementation registered.
StringUtils stringUtils = CompatLocator.getStringUtils();
if (stringUtils == null) {
throw new ReferenceNotRegisteredException(CompatLocator.STRINGUTILS_NAME,
CompatLocator.getInstance());
}
}
}
Nota
Como norma general, al emplear el mecanismo de inicialización de librerías de libTools deberemos seguir un par de normas básica para no tener problemas de orden y dependencias en la inicialización:
- El registro de implementaciones en los Locator se hará siempre en el método doInitialize().
- El acceso a referencias a través de algún Locator en un Library, se hará siempre en el método doPostInitialize().
Con esto nos aseguraremos que, siempre que se acceda a una referencia a través de un Locator, se habrá registrado la implementación correspondiente.
Tests Unitarios
Dado que con el uso de los Locator estamos reforzando el uso de interfaces, y la separación de implementaciones alternativas u opcionales, podemos aprovechar esto a la hora de realizar los tests unitarios.
La forma de aprovecharlo consiste en crear los tests unitarios que comprueben el funcionamiento correcto a nivel de API, dentro del proyecto que incluya la definición de los interfaces. Dichos tests pueden crearse como clases abstractas, que definan métodos para crear los objetos que implementan los interfaces a probar. Así, cada implementación particular podrá crear sus tests unitarios, simplemente heredando de los tests del API, e implementando el método de creación de sus clases propias.
Con esta estructura de tests unitarios podemos definir los tests unitarios a nivel de API, y lanzarlos de forma sencilla para cada implementación.
Basándonos en el ejemplo anterior, la distribución de tests unitarios quedaría de la siguiente forma en las tres librerías del ejemplo:
libCompat:
public abstract class StringUtilsTestAbstract extends TestCase {
private StringUtils utils;
protected void setUp() throws Exception {
super.setUp();
utils = createUtils();
}
protected abstract StringUtils createUtils();
public void testReplaceAll() {
String testString = "En un lugar de la Mancha";
String resultString = "En_un_lugar_de_la_Mancha";
String regex = " ";
String replacement = "_";
assertEquals(resultString,
utils.replaceAll(testString, regex, replacement));
}
}
libCompatSE:
public class StandardStringUtilsTest extends StringUtilsTestAbstract {
protected StringUtils createUtils() {
return new StandardStringUtils();
}
}
libCompatME:
public class MobileStringUtilsTest extends StringUtilsTestAbstract {
protected StringUtils createUtils() {
return new MobileStringUtils();
}
}
Notas adicionales
Las excepciones del Locator y del Library son de tipo RuntimeException por lo que, aunque declaradas en su API, no obligan a ser capturadas mediante un try ... catch. A nivel de aplicación, si que habrá que hacer un tratamiento de estas excepciones, para mostrar al usuario algún tipo de información sobre el error producido.
No se han definido excepciones normales, ya que son errores graves y no se puedan tratar desde código. Así no obligamos a capturarlas en cada lugar dónde se use un Locator o Library, y se pueden tratar en un nivel superior.