viernes, 11 de diciembre de 2009

Iterando sobre estructuras complejas de mapas

¿Te has encontrado alguna vez con estructura de datos anidadas y para colmo con un colocho de genéricos ("Generics")? Aún peor ¿has sido el autor intelectual de estas sentencias confusas? Dejame mostrar un ejemplo de lo que me refiero y que he encontrado al interactuar con las prácticas "inusuales" de algunos compañeros de proyecto.

En nuestra aplicación de ejemplo necesitamos guardar y mostrar la información de matrícula para un cuatrimestre de universidad. La información está estructurada de una manera jerárquica donde el primer nodo es el año en curso, luego le siguen los cuatrimestres, seguidamente los cursos y en el nivel más bajo los estudiantes enlistados en esos cursos.

Año (Year)
++ Cuatrimestre (Quatrimester)
+++ Curso (Course)
++++ Estudiante (Student)

Pareciera que podríamos sentarnos con una buena tacita de café, un hoja de papel y lapiz para sentarnos gustosamente a diseñar nuestras clases; bueno voy a mostrar lo que puede pasar
cuando alguien quiere brincarse este paso y "ahorrar" un poco de tiempo de diseño:


Map<Integer, Map<Integer, Map<String, List<Student>>>> studentsByQuarter = new HashMap<Integer, Map<Integer, Map<String, List<Student>>>>();


¿Para qué complicar más nuestra aplicación agregando más archivos para representar nuestras clases cuando todo se puede resumir en una sola linea? Ese es el acercamiento implicito al problema cuando alguien decide crear este nivel de complejidad que para él o ella puede ser comprendible pero pobre de la persona que tenga que mantener ese código y comenzar por comprender ese colocho.

Voy a desarrollar más esta manera de codificar para mostrar la complejidad intrínseca que se obtiene al no aislar los distintos componentes del problema que queremos resolver.

La clase "Student" as bastante simple con solamente un atributo "fullName":


public class Student {
private String fullName;

public Student(String fullName) {
this.fullName = fullName;
}

public String getFullName() {
return fullName;
}

public void setFullName(String fullName) {
this.fullName = fullName;
}
}


Ahora agregamos un clase "EnrollmentProcessVr1" con la ya introducidad variable "monstruo" y dos métodos adicionales:

+ public static void startEnrollment(int year, int quarter){...} // ¨Para guardar los datos de matrícula.
+ public static void showEnrollment() {...} // Muestra los datos contenidos en la estructura de datos.

Veamos lo complicado que se vuelve tanto agregar como mostrar datos desde esta estructura:


import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class EnrollmentProcessVr1 {
private static Map<Integer, Map<Integer, Map<String, List<Student>>>> studentsByQuarter =
new HashMap<Integer, Map<Integer, Map<String, List<Student>>>>();

public static void startEnrollment(int year, int quarter) {
// Create a Year.
Integer currentYear = new Integer(year);

// Create a quatrimester.
Integer quatrimester = new Integer(quarter);

// Create courses.
String course1 = "Discrete Maths";
String course2 = "Programming I";

// Create List of Students.
List<Student> students = new ArrayList<Student>();
students.add(new Student("Cristobal Colón"));
students.add(new Student("Juan SantaMaría"));

// Create a Map of Courses/Students.
Map <String, List<Student>> studentsByCourses = new HashMap<String, List<Student>>();

// Add Students to courses.
studentsByCourses.put(course1, students);
studentsByCourses.put(course2, students);

// Create a Map of Quarter/Courses.
Map<Integer, Map <String, List<Student>>> coursesQuarterMap =
new HashMap<Integer, Map<String,List<Student>>>();

coursesQuarterMap.put(quatrimester, studentsByCourses);

// Add quarter to year.
studentsByQuarter.put(currentYear, coursesQuarterMap);
}

public static void showEnrollment() {
Iterator studentsByQuarterIterator = studentsByQuarter.entrySet().iterator();
while (studentsByQuarterIterator.hasNext()) {
Map.Entry studentsByQuarterPairs = (Map.Entry)studentsByQuarterIterator.next();
System.out.println("Year: " + String.valueOf(studentsByQuarterPairs.getKey()));

Map<Integer, Map<String, List<Student>>> quarterCoursesMap =
(Map<Integer, Map<String, List<Student>>>) studentsByQuarterPairs.getValue();

Iterator quarterCoursesIterator = quarterCoursesMap.entrySet().iterator();

while (quarterCoursesIterator.hasNext()) {
Map.Entry coursesByQuarterPairs = (Map.Entry)quarterCoursesIterator.next();
System.out.println("\tQuarter: " + String.valueOf(coursesByQuarterPairs.getKey()));

Map<String, List<Student>> coursesStudentsMap =
(Map<String, List<Student>>) coursesByQuarterPairs.getValue();

Iterator coursesStudentsIterator = coursesStudentsMap.entrySet().iterator();

while (coursesStudentsIterator.hasNext()) {
Map.Entry coursesStudentsPairs = (Map.Entry)coursesStudentsIterator.next();
System.out.println("\t\tCourse: " + String.valueOf(coursesStudentsPairs.getKey()));

List<Student> studentsByCourse = (List<Student>) coursesStudentsPairs.getValue();

for(Student student : studentsByCourse) {
System.out.println("\t\t\tStudent: " + student.getFullName());
}
}
}
}
}

public static void main(String[] args) {
EnrollmentProcessVr1.startEnrollment(2009, 3);
EnrollmentProcessVr1.showEnrollment();
}
}


Salida del método main:



Mi foco en este post será mostrar una manera de resolver la complejidad intrisica de manejar estructuras de mapa, no necesariamente como hacer "refactoring" de un mal diseño de estructura de datos, pero el desarrollo de este ejemplo ayuda a probar un punto de que incluso con un buen diseño de clases podemos tener mucho código para hacer un recorrido de las estructuras internas de mapa de nuestras clases.

Volvamos entonces al modo diseño de clases:




EnrollmentTrackVr1:


import java.util.LinkedHashMap;
import java.util.Map;

public class EnrollmentTrackVr1 {
private Map<Integer, QuatrimestersVr1> quatrimesters;

public EnrollmentTrackVr1() {
quatrimesters = new LinkedHashMap<Integer, QuatrimestersVr1>();
}

public Map<Integer, QuatrimestersVr1> getQuatrimesters() {
return quatrimesters;
}


public void addStudent(Student student, int year, int quatrimesterNumber, String courseName) {
if (!quatrimesters.containsKey(year)) {
quatrimesters.put(year, new QuatrimestersVr1());
}
quatrimesters.get(year).addCourse(quatrimesterNumber, courseName, student);
}
}



QuatrimestersVr1:


import java.util.HashMap;
import java.util.Map;

public class QuatrimestersVr1 {

private Map<Integer, CoursesVr1> courses;

public QuatrimestersVr1() {
courses = new HashMap<Integer, CoursesVr1>();
}

public void addCourse(int quatrimesterNumber, String courseName, Student student) {
if (!courses.containsKey(quatrimesterNumber)) {
courses.put(quatrimesterNumber, new CoursesVr1());
}
courses.get(quatrimesterNumber).addStudent(courseName, student);
}

public Map<Integer, CoursesVr1> getCourses() {
return courses;
}
}


CoursesVr1:


import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


public class CoursesVr1 {
private Map<String, List<Student>> students;

public CoursesVr1() {
students = new HashMap<String, List<Student>>();
}

public void addStudent(String courseName, Student student) {
if (!students.containsKey(courseName)) {
students.put(courseName, new ArrayList<Student>());
}
students.get(courseName).add(student);
}

public Map<String, List<Student>> getStudents() {
return students;
}

}


En la nueva versión de la clase EnrollmentProcess vemos como el código para agregar nueva información se simplifica significativamente pero la iteración de la estructura
sigue siendo un código a mi gusto complejo porque se necesita accesar los iteradores de cada una de las estructuras internas de cada clase de nuestro diseño:


import java.util.Iterator;
import java.util.List;
import java.util.Map;


public class EnrollmentProcessVr2 {
private static EnrollmentTrackVr1 enrollmentTrackVr1;

public static void startEnrollment(int year, int quarter) {

String course1 = "Discrete Maths";
String course2 = "Programming I";
Student student1 = new Student("Cristobal Colón");
Student student2 = new Student("Juan Santamaría");

enrollmentTrackVr1 = new EnrollmentTrackVr1();
enrollmentTrackVr1.addStudent(student1, year, quarter, course1);
enrollmentTrackVr1.addStudent(student2, year, quarter, course1);
enrollmentTrackVr1.addStudent(student1, year, quarter, course2);
enrollmentTrackVr1.addStudent(student2, year, quarter, course2);

}

public static void showEnrollment() {
Iterator enrollmentIterator =
enrollmentTrackVr1.getQuatrimesters().entrySet().iterator();

while (enrollmentIterator.hasNext()) {
Map.Entry enrollmentPairs = (Map.Entry)enrollmentIterator.next();
System.out.println("Year: " + String.valueOf(enrollmentPairs.getKey()));

QuatrimestersVr1 quatrimesters =
(QuatrimestersVr1) enrollmentPairs.getValue();

Iterator quatrimestersIterator =
quatrimesters.getCourses().entrySet().iterator();

while (quatrimestersIterator.hasNext()) {
Map.Entry quatrimestersPairs = (Map.Entry)quatrimestersIterator.next();
System.out.println("\tQuarter: " + String.valueOf(quatrimestersPairs.getKey()));

CoursesVr1 courses = (CoursesVr1) quatrimestersPairs.getValue();

Iterator coursesIterator =
courses.getStudents().entrySet().iterator();

while (coursesIterator.hasNext()) {
Map.Entry coursesPairs = (Map.Entry)coursesIterator.next();
System.out.println("\t\tCourse: " + String.valueOf(coursesPairs.getKey()));

List<Student> studentsByCourse = (List<Student>) coursesPairs.getValue();

for(Student student : studentsByCourse) {
System.out.println("\t\t\tStudent: " + student.getFullName());
}
}
}
}
}

public static void main(String[] args) {
EnrollmentProcessVr2.startEnrollment(2009, 3);
EnrollmentProcessVr2.showEnrollment();
}
}



¿No sería más pura vida que nuestras clases pudieran ser iteradas en la misma manera que hacemos con una lista de Java Collections?


List<String> fooList = new ArrayList<String>();
...
for (String fooObject : fooList) {
System.out.println(fooObject);
}


Es posible hacer esto si aprendemos a usar las interfaces "Iterable" y "Iterator". En el diseño que voy a mostrar a continuación uso mi propia interface la cual extiende de estas dos y además agregamos un nuevo método que nos será útil para retornar la llave del objeto en curso que estamos iterando (explicaré en detalle más adelante).

Así es como luce el nuevo diseño:



La nueva interface recibe un nuevo elemento genérico "K" que será el tipo/clase de la llave del mapa.


import java.util.Iterator;

public abstract interface MapIterable<K, V> extends Iterable<V>, Iterator<V>{
public K currentKey();
}


Explicamos a continuación en detalle cuáles métodos necesitamos implementar para que nuestras clases sean iterables:

+ public boolean hasNext() : este método indica si la clase puede avanzar en la estructura que está iterando. En mi implementación agrego una variable privada (currentKeyYearIndex)
para mantener el estado del índice actual del arreglo. Esta variable es incrementada con el método next().

+ public V next(): retorna el siguiente objeto de la iteración. En esta implementación obtengo el keyset del mapa interno de la clase y lo paso a un arreglo para obtener el elemento
de la locación indicada por el índice actual.

+ public void remove(): remueve el objeto actual. No necesitaba esta funcionalidad así que solamente arrojo una excepción de tipo UnsupportedOperationException;

+ public Iterator iterator(): Obtiene el iterador. Simplemente retorna la clase.

+ public K currentKey(): el nuevo método que agregué extra para retornar la llave actual de la iteración.

EnrollmentTrackVr2:


import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

public class EnrollmentTrackVr2 implements MapIterable<Integer, QuatrimestersVr2> {

private Map<Integer, QuatrimestersVr2> quatrimesters;
private int currentKeyYearIndex;

public EnrollmentTrackVr2() {
quatrimesters = new LinkedHashMap<Integer, QuatrimestersVr2>();
currentKeyYearIndex = 0;
}

public Map<Integer, QuatrimestersVr2> getQuatrimesters() {
return quatrimesters;
}


public void addStudent(Student student, int year, int quatrimesterNumber, String courseName) {
if (!quatrimesters.containsKey(year)) {
quatrimesters.put(year, new QuatrimestersVr2());
}
quatrimesters.get(year).addCourse(quatrimesterNumber, courseName, student);
}

public boolean hasNext() {
if (quatrimesters.keySet().toArray().length > currentKeyYearIndex) {
return true;
}
return false;
}

public QuatrimestersVr2 next() {
int currentKeyYear = (Integer)
quatrimesters.keySet().toArray()[currentKeyYearIndex++];
return quatrimesters.get(currentKeyYear);
}

public void remove() {
throw new UnsupportedOperationException();
}

public Iterator<QuatrimestersVr2> iterator() {
return this;
}
public Integer currentKey() {
return (Integer)
quatrimesters.keySet().toArray()[currentKeyYearIndex-1];
}
}


QuatrimestersVr2:


import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class QuatrimestersVr2 implements MapIterable<Integer, CoursesVr2> {

private Map<Integer, CoursesVr2> courses;
private int currentQuatriIndex;
private int year;

public QuatrimestersVr2() {
courses = new HashMap<Integer, CoursesVr2>();
currentQuatriIndex = 0;
}

public QuatrimestersVr2(int year) {
this();
this.year = year;
}

public void addCourse(int quatrimesterNumber, String courseName, Student student) {
if (!courses.containsKey(quatrimesterNumber)) {
courses.put(quatrimesterNumber, new CoursesVr2());
}
courses.get(quatrimesterNumber).addStudent(courseName, student);
}

public Map<Integer, CoursesVr2> getCourses() {
return courses;
}

public boolean hasNext() {
if (courses.keySet().toArray().length > currentQuatriIndex) {
return true;
}
return false;
}

public CoursesVr2 next() {
int currentQuatri = (Integer)
courses.keySet().toArray()[currentQuatriIndex++];
return courses.get(currentQuatri);
}

public void remove() {
throw new UnsupportedOperationException();
}

public Iterator<CoursesVr2> iterator() {
return this;
}

public int getYear() {
return year;
}

public void setYear(int year) {
this.year = year;
}

public Integer currentKey() {
return (Integer)
courses.keySet().toArray()[currentQuatriIndex-1];
}
}



CoursesVr2:


import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class CoursesVr2 implements MapIterable<String, List<Student>> {

private Map<String, List<Student>> students;
private int currentCourseIndex;
private int quatrimester;

public CoursesVr2() {
students = new HashMap<String, List<Student>>();
currentCourseIndex = 0;
}

public CoursesVr2(int quatrimester) {
this();
this.quatrimester = quatrimester;
}

public void addStudent(String courseName, Student student) {
if (!students.containsKey(courseName)) {
students.put(courseName, new ArrayList<Student>());
}
students.get(courseName).add(student);
}

public Map<String, List<Student>> getStudents() {
return students;
}

public boolean hasNext() {
if (students.keySet().toArray().length > currentCourseIndex) {
return true;
}
return false;
}

public List<Student> next() {
String currentCourse =
students.keySet().toArray()[currentCourseIndex++].toString();
return students.get(currentCourse);
}

public void remove() {
throw new UnsupportedOperationException();
}

public Iterator<List<Student>> iterator() {
return this;
}

public int getQuatrimester() {
return quatrimester;
}

public void setQuatrimester(int quatrimester) {
this.quatrimester = quatrimester;
}

public String currentKey() {
return students.
keySet().toArray()[currentCourseIndex-1].toString();
}
}



Finalmente veamos como queda nuestro proceso de iteración:

EnrollmentProcessVr3:


import java.util.List;

public class EnrollmentProcessVr3 {
private static EnrollmentTrackVr2 enrollmentTrackVr2;

public static void startEnrollment(int year, int quarter) {

String course1 = "Discrete Maths";
String course2 = "Programming I";
Student student1 = new Student("Cristobal Colón");
Student student2 = new Student("Juan Santamaría");

enrollmentTrackVr2 = new EnrollmentTrackVr2();

enrollmentTrackVr2.addStudent(student1, year, quarter, course1);
enrollmentTrackVr2.addStudent(student2, year, quarter, course1);

enrollmentTrackVr2.addStudent(student1, year, quarter, course2);
enrollmentTrackVr2.addStudent(student2, year, quarter, course2);

}

public static void showEnrollment() {
for(QuatrimestersVr2 quatrimesters : enrollmentTrackVr2) {
System.out.println("Year: " + enrollmentTrackVr2.currentKey());
for (CoursesVr2 courses : quatrimesters) {
System.out.println("\tQuatrimester: " + quatrimesters.currentKey());
for (List<Student> students : courses) {
System.out.println("\t\tCourses: " + courses.currentKey());
for (Student student : students) {
System.out.println("\t\t\tStudent: " + student.getFullName());
}
}
}
}
}

public static void main(String[] args) {
EnrollmentProcessVr3.startEnrollment(2009, 3);
EnrollmentProcessVr3.showEnrollment();
}
}



El código puede que tenga algunas pulgas, creo que olvidé reinicializar la llave actual en algu punto, pero la idea no era tener un código a prueba de balas sino demostrar el poder de usar la interface "Iterable". Espero haya sido de interés este tema y ojalá puedan poner en práctica el uso de Iterable.

No hay comentarios:

Publicar un comentario