Skip to content

Commit 07c799c

Browse files
committed
Completa la exposición sobre el patrón Factory
1 parent 5ae6dde commit 07c799c

File tree

2 files changed

+202
-1
lines changed

2 files changed

+202
-1
lines changed

source/98.apendices/05.pattern.rst

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ de la traducción tendrá que implementar la siguiente interfaz:
7373
.. literalinclude:: files/Traductor.java
7474
:language: java
7575
:class: toggle
76+
:caption: Interface Traductor
7677
:start-at: public interface
7778

7879
en la que hemos supuesto que los datos los almacenamos en memoria en forma de
@@ -104,11 +105,81 @@ habrá más que hacer lo siguiente:
104105
traductor.escribir(System.out, data);
105106
106107
Obsérvese que el código de ``TraductorFactory`` depende de qué clases
107-
traductoras hallamos creado realmente; y, si se crea una nueva (o se elimina una
108+
traductoras hayamos creado realmente; y, si se crea una nueva (o se elimina una
108109
ya creada por algún motivo), habrá que editar la clase para que se refleje este
109110
cambio. El siguiente apartado complica un poco la implementación, pero permite
110111
escribir una clase sin esta dependencia, de manera que podemos reaprovecharla,
111112
sea cual sea el caso.
112113

113114
Automatización
114115
--------------
116+
La idea es evitar que tengamos que escribir y reescribir la clase que
117+
implementa el patrón Factory constantemente; y además crear un código que nos
118+
sirva en cualquier aplicación:
119+
120+
.. literalinclude:: files/Factory.java
121+
:language: java
122+
:class: toggle
123+
:caption: Clase Factory
124+
:start-at: public class
125+
126+
La principal diferencia (además de ser mucho más complicado que el anterior) es
127+
que esta clase sí se instancia con el constructor ``Factory(String packageName,
128+
Class<I> interfaceClass)`` para indicar en qué paquete se encuentran las clases
129+
que implementan la interfaz y cuál es esta. En el ejemplo de nuestros
130+
traductores:
131+
132+
.. code-block:: java
133+
134+
Factory<Traductor> trFactory = new Factory<>("edu.acceso.ejemplo.traductor", Traductor.class);
135+
136+
La creación de este objeto *Factory* provoca que gracias a la reflexión se
137+
analice el paquete y se localicen las clases que implementan la interfaz
138+
(``Traductor`` en este caso). El objetivo es relacionar estas clases con su
139+
nombre y, en su caso, con los nombres alternativos contenidos en el atributo
140+
``aliases``. Por ejemplo:
141+
142+
.. code-block:: java
143+
144+
public class TYaml implements Traductor {
145+
// También podría ser String si sólo hay una alternativa
146+
private static final String[] = {"yml", "yaml"};
147+
148+
// Implementación de la traducción para YAML...
149+
}
150+
151+
152+
Esta definición relacionaría las cadenas "`tyaml`", "`yml`" y "`yaml`" con la
153+
clase ``TYaml``, por lo que, cuando quisiéramos crear el objeto adecuado de
154+
traducción, nos bastaría con hacer:
155+
156+
.. code-block:: java
157+
158+
String formato = "yml"; // Por ejemplo.
159+
160+
Factory<Traductor> trFactory = new Factory<>("edu.acceso.ejemplo.traductor", Traductor.class);
161+
Traductor traductor = trFactory.getObject(formato);
162+
163+
La clase tiene, además, dos métodos adicionales que sirven básicamente para
164+
brindar información:
165+
166+
``getClasses()``
167+
que devuelve un mapa en que las claves son las cadenas y los valores las
168+
clases correspondientes.
169+
170+
``getAliasesByClass()``
171+
que devuelve un mapa en que cada clave es una clase y los valores un array
172+
con todos los nombres con los que está relacionada.
173+
174+
.. warning:: El ćodigo tiene una limitación: el constructor de las clases no
175+
puede tener argumentos. En caso de que debiera tenerlos, no obstante, es
176+
fácil soslayar la limitación: bastaría con incluir en la interfaz un método
177+
inicializador. Por ejemplo:
178+
179+
.. code-block:: java
180+
181+
public interface Foobar {
182+
183+
public void initialize( /* Los argumentos que sea */ );
184+
// Resto de métodos que definen la interfaz.
185+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package edu.acceso.almacenestudiantes.core;
2+
3+
import java.lang.reflect.Field;
4+
import java.lang.reflect.InvocationTargetException;
5+
import java.util.AbstractMap;
6+
import java.util.ArrayList;
7+
import java.util.Arrays;
8+
import java.util.Collections;
9+
import java.util.HashMap;
10+
import java.util.List;
11+
import java.util.Map;
12+
import java.util.stream.Collectors;
13+
14+
import org.reflections.Reflections;
15+
import org.slf4j.Logger;
16+
import org.slf4j.LoggerFactory;
17+
18+
/**
19+
* Implementa el patrón Factory para escoger un objeto adecuado
20+
*/
21+
public class Factory<I> {
22+
private static final Logger logger = LoggerFactory.getLogger(Factory.class);
23+
private final static String alias = "alias";
24+
/*
25+
* Mapa cuyas claves son los nombres asociados a una clase
26+
* que implementa la interfaz {@link I} y cuyos valores son las propias clases
27+
* que implementan dicha interfaz.
28+
*/
29+
private final Map<String, Class<? extends I>> classes;
30+
31+
/**
32+
* Escanea un paquete para obtener clases.
33+
* @param <I> El tipo de la interfaz.
34+
* @param packageName El nombre del paquete.
35+
* @param interfaceClass La interfaz que implementan todas las clases que se quieren encontrar.
36+
*/
37+
public Factory(String packageName, Class<I> interfaceClass) {
38+
Reflections reflections = new Reflections(packageName);
39+
40+
classes = reflections.getSubTypesOf(interfaceClass)
41+
.stream()
42+
.flatMap(clazz -> getAliases(clazz).stream().map(alias -> new AbstractMap.SimpleEntry<>(alias, clazz)))
43+
.collect(Collectors.toMap(
44+
Map.Entry::getKey,
45+
Map.Entry::getValue,
46+
(existing, replacement) -> existing,
47+
HashMap::new
48+
));
49+
}
50+
51+
/**
52+
* Lista los nombres por los que se puede referir una clase (el propio nombre
53+
* de la clase más todos los que contenga el atributo alias)
54+
* @param clazz La clase que se inspecciona
55+
* @return El listado de clases.
56+
*/
57+
private static List<String> getAliases(Class<?> clazz) {
58+
List<String> aliases = new ArrayList<>(List.of(clazz.getSimpleName()));
59+
60+
try {
61+
Field field = clazz.getDeclaredField(alias);
62+
if(!field.canAccess(null)) field.setAccessible(true);
63+
64+
Object value = field.get(null);
65+
66+
if(value instanceof String && !((String) value).isBlank()) {
67+
aliases.add((String) value);
68+
}
69+
else if(value instanceof String[]) {
70+
aliases.addAll(Arrays.asList((String[]) value));
71+
}
72+
}
73+
catch(NoSuchFieldException | IllegalAccessException err) {}
74+
75+
return aliases.stream()
76+
.filter(alias -> alias != null)
77+
.map(alias -> alias.trim().toLowerCase())
78+
.filter(alias -> !alias.isBlank())
79+
.distinct()
80+
.toList();
81+
}
82+
83+
/**
84+
* Obtiene el traductor.
85+
* @param formato El formato en el que se quiere traducir.
86+
* @return El objeto traductor.
87+
*/
88+
@SuppressWarnings("null")
89+
public I getObject(String formato) {
90+
Class<? extends I> clazz = classes.get(formato.toLowerCase());
91+
92+
if(clazz == null) {
93+
String formatos = classes.keySet().stream().collect(Collectors.joining("\n - "));
94+
logger.error("'{}': Formato desconocido. Disponibles:\n - {}", formato, formatos);
95+
System.exit(2);
96+
}
97+
try {
98+
return clazz.getDeclaredConstructor().newInstance();
99+
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
100+
| NoSuchMethodException | SecurityException e) {
101+
throw new RuntimeException(e);
102+
}
103+
}
104+
105+
/**
106+
* Obtiene un mapa que asocia cada clases con su lista de alias a partir del mapa que relaciona cada alias con su clase corrspondiente
107+
* @param <T> El tipo de la interfaz
108+
* @return Otro mapa en que cada clase está relacionada con todos los alias que tiene.
109+
*/
110+
public Map<Class<? extends I>, String[]> getAliasesByClass() {
111+
return classes.entrySet().stream()
112+
.collect(Collectors.groupingBy(
113+
Map.Entry::getValue,
114+
Collectors.mapping(
115+
Map.Entry::getKey,
116+
Collectors.collectingAndThen(
117+
Collectors.toList(),
118+
list -> {
119+
Collections.sort(list);
120+
return list.toArray(String[]::new);
121+
}
122+
)
123+
)
124+
));
125+
}
126+
127+
public Map<String, Class<? extends I>> getClasses() {
128+
return classes;
129+
}
130+
}

0 commit comments

Comments
 (0)