Desarrollo de UI con componentes: Implementación de la búsqueda by-example @Deprecated


Objetivo de la clase

Entender cómo está modelada la búsqueda nos permitirá
  • repasar
    • delegación y polimorfismo
    • ideas de diseño (como template method)
    • "pensar en la interfaz y no en la implementación"
  • aplicar esas ideas en el ejemplo del Videoclub hace fácil que la búsqueda de socios se haga
    • sobre una colección en memoria
    • sobre una base de objetos open-source llamada DB4O
Nuestro foco no estará puesto en cómo se implementa la tecnología de persistencia (la que inserta, actualiza o recupera la información en un medio que eventualmente sobreviva a la aplicación), sino en ver de qué manera el objeto de aplicación delega esa tarea a diferentes objetos que acceden a los datos, también llamados "homes" porque es el lugar donde viven esos objetos.

Disparando la búsqueda

Binding del botón buscar

Recordemos el binding del botón Buscar de una SearchWindow:
  • el evento onClick se bindea contra el método search del modelo
  • ¿quién es el modelo?
    • cualquier objeto o subclase de Search<T>. En el ejemplo de los celulares podés ver cómo se implementa una búsqueda "a mano".
    • SearchByExample<Socio> hereda de Search<T>, así que la búsqueda by-example es un caso particular de búsqueda. Se utiliza un objeto example como prototipo y seteamos los atributos que intervienen en la búsqueda.

Template method: search / doSearch

Tenemos un Template Method en Search<T> delegando la implementación del doSearch a sus subclases. Los resultados de la búsqueda se almacenan en la propiedad results y hay otras cosas que pasan que no nos importa mucho comentar:


>>Search<T>

    /**
* Perform the search. The results will be left in the {@link #RESULTS} property.
*/
public void search() {
this.results = null;
this.results = this.doSearch();

// log
StringBuilder builder = new StringBuilder("Search Result: ");
for (T object : this.results) {
builder.append(object + ", ");
}
log.debug(builder);
// endlog

// Adjust selection, nullify if selected object is out-filtered of the new search.
if (!this.results.contains(this.getSelected())) {
this.setSelected(null);
}
}


El doSearch de SearchByExample delega la búsqueda a la home:
protected List<T> doSearch() {
    return this.home.buscarByExample(this.example);
}

Codificar una búsqueda genérica

Home<T> provee una interfaz donde viven muchos objetos T. El home de socios extiende de CollectionBasedHome<Socio> (que implementa una colección en memoria). Si tuviéramos que programar la búsqueda, deberíamos escribir algo como:

>>HomeSocios
public List<Socio> buscarByExample(Socio example) {
    List<Socio> result = new ArrayList<Socio>();
    for (Socio socio : this.socios) {
        if (socio.cumpleLoQueQuiero(example)) {
            result.add(socio);
        }
    }
    return result;
}

Repasemos las cosas que hacer el buscarByExample nuestro:
  • inicializar la lista de resultados
  • iterar sobre una colección de elementos original
    • preguntar si cada elemento de la colección cumple "lo que quiero" (determinado por el example)
  • si cumple: lo agrego a la colección resultante
  • devuelvo el resultado
Sólo lo que está en negrita es particular de cada búsqueda. Si quisiéramos subir el método buscarByExample a CollectionBasedHome, ya no hablamos de Socio, sino de T. Otra cosa que vamos a hacer es no preguntarle al socio si cumpleLoQueQuiero, sino delegar esa respuesta en la home misma, cambiando el objeto receptor:

>>CollectionBasedHome<T> v1.0
public List<T> buscarByExample(T example) {
    List<T> result = new ArrayList<T>();
    for (T object : this.objects) {
        if (this.cumpleLoQueQuiero(object, example)) {
            result.add(object);
        }
    }
    return result;
}

De esta manera generamos un nuevo template method: el "cumpleConLoQueQuiero" es responsabilidad de cada Home.

Más declaratividad

Utilizar Commons Collections de Jakarta permite desprenderse del for + if aumentando la declaratividad:

>>CollectionBasedHome v2.0
public List<T> searchByExample(final T example) {
return (List<T>) CollectionUtils.select(this.objects, this.getCriterio(example));
}

  • Escribo menos, digo qué tiene que cumplirse.
  • El algoritmo (el for) queda en Commons Collections, para lo cual tengo que decirle la lista original de elementos sobre los cuales voy a filtrar y el criterio por el cual voy a incorporar o no los elementos de la colección en una nueva
  • El "cumpleLoQueQuiero" es el getCriterio que cambia la interfaz (solamente recibe el example)
Si vemos la implementación de Commons Collections, no nos asombra ver lo que hicieron:

public static void select(Collection inputCollection, Predicate predicate, Collection outputCollection) {
    for (Iterator iter = inputCollection.iterator(); iter.hasNext();) {
        Object item = iter.next();
        if (predicate.evaluate(item)) {
            outputCollection.add(item);
        }
    }
}

Reificación de los criterios (filtros)

Qué pasa con los atributos de example
  • si un atributo es nulo, no participa de la búsqueda
  • los atributos no nulos tengo que definir yo cómo quiero que llenen el criterio de búsqueda: búsqueda exacta, comienza por, contiene, y si me importa distinguir mayúsculas de minúsculas, etc. 
Cada decisión que tomo con los atributos lo puedo abstraer en un objeto que representa un "Criterio" de búsqueda
  • que el socio comience con el nombre "ALF"
  • que el alumno tenga exactamente el legajo 34129
  • que el colectivo pase por Constitución
  • etc.etc.etc.
El criterio se modela como un objeto que implementa un método evaluate, que recibe un objeto de la colección y devuelve un booleano (true: lo agrego a la colección resultante). Entonces tenemos que definir -ahora sí, en nuestro Home- el criterio para hacer la búsqueda by example:

@Override protected Predicate getCriterio(final Socio socioBuscado) {
...
}

Si el usuario no ingresó parámetros de búsqueda, todos me interesan, esto se logra definiendo un criterio como el siguiente:
protected Predicate getCriterioTodas() {
    return new Predicate() {
        public boolean evaluate(Object arg) {
            return true;
        }
    };
}

Este método no hay que escribirlo, lo heredamos de CollectionBasedHome. 

Actualizamos cómo va quedando el getCriterio original:

@Override protected Predicate getCriterio(final Socio unSocio) { Predicate resultPredicate = this.getCriterioTodas(); return resultPredicate; }


unSocio (o socioBuscado en los criterios) es el example original. 

Para filtrar por nombre hay que generar un criterio que matchee todo lo que contenga el String que ingresó el usuario (sin distinguir mayúsculas de minúsculas):

protected Predicate<Socio> getCriterioSocioPorNombre(final Socio socioBuscado) {
return new Predicate() {
@Override
public boolean evaluate(Object arg) {
Socio unSocio = (Socio) arg;
return unSocio.getNombre().toLowerCase().contains(socioBuscado.getNombre().toLowerCase());
}
};
}

El Predicate es una interfaz, pero en Java podemos definir "clases anónimas", que se usan en un solo contexto. Basta con implementar los métodos de esa interfaz

Si quisiéramos, podríamos cambiar la búsqueda a "comienza con" así:

protected Predicate<Socio> getCriterioSocioPorNombre(final Socio socioBuscado) {
return new Predicate() {
@Override
public boolean evaluate(Object arg) {
Socio unSocio = (Socio) arg;
return unSocio.getNombre().toLowerCase().startsWith(socioBuscado.getNombre().toLowerCase());
}
};
}

Para el estado, la coincidencia debe ser exacta:

private Predicate<Socio> getCriterioSocioPorEstado(final Socio socioBuscado) { return new Predicate() { @Override public boolean evaluate(Object arg) { Socio unSocio = (Socio) arg; return unSocio.getEstado() == null || unSocio.getEstado().equals(socioBuscado.getEstado()); } }; } 

¿Y si cargaron nombre y estado? Tendría que inventar un tercer criterio, eso no está tan bueno (si tengo 3 campos de búsqueda tengo muchas opciones posibles: axbxc, a, b, c, axb, bxc, axc y esto se incrementa con cada campo de búsqueda que agrego), por suerte existe un AndPredicate, que recibe dos criterios y busca que ambos criterios se cumplan.

Entonces por cada campo de búsqueda podemos definir un criterio extra y "concatenarlo" al criterio existente:

public Predicate<Socio> getCriterio(final Socio unSocio) { Predicate<Socio> resultPredicate = this.getCriterioTodas(); String nombre = unSocio.getNombre(); String direccion = unSocio.getDireccion(); Integer id = unSocio.getId(); Estado estado = unSocio.getEstado(); if (id != null) { resultPredicate = new AndPredicate<Socio>(resultPredicate, this.getCriterioPorId(id)); } if (nombre != null) { resultPredicate = new AndPredicate<Socio>(resultPredicate, this.getCriterioSocioPorNombre(unSocio)); } if (direccion != null) { resultPredicate = new AndPredicate<Socio>(resultPredicate,this.getCriterioSocioPorDireccion(unSocio)); } if (estado != null) { resultPredicate = new AndPredicate<Socio>(resultPredicate, this.getCriterioSocioPorEstado(unSocio)); }

return resultPredicate; }


Diagrama general

Repasamos la solución en el diagrama de clases:


RepositorioCelulares se reemplaza por la home que estás construyendo.