Script Ejercicio Voluntariado

Casos de uso

Principales

  • Postularse como voluntario
    • Validaciones sobre postulante
    • Validaciones sobre la tarea
    • Postularse (separar voluntarios reales de postulados posibles)
    • Acciones post-postulación
  • Cierre automático inscripción de voluntarios
    • Cancelación de tareas que no llegan a cupo mínimo/Cierre de las que llegan al cupo
  • Cierre manual inscripción de voluntarios, debe llamar a cancelar/"comenzar" una tarea
  • Conocer el % de avance de un proyecto/tarea

Periféricos (no obligatorios)

  • Crear un proyecto
  • Crear una tarea

Postularse como voluntario

Objeto receptor: una tarea

Objetos candidatos:

  • Tarea (composite: tareas simples-operativas y compuestas-de coordinación)
  • Tipo de postulación (strategy asociado a la tarea): manual, automática o semiautomática
  • Postulante
  • módulo RRHH
  • interesados en acciones post-postulación

La validación de las tareas operativas y de coordinación lo resolvemos con un Template Method.

>>Tarea (abstracta)
public void postularse(Postulante postulante) {
    if (!this.isPlanificada()) {
        throw new BusinessException("La tarea no está en estado planificada");
    }
    this.validarPostulante(postulante);
    this.tipoPostulacion.validarPostulante(postulante, this);
    this.agregarPostulante(postulante);
    for (PostulacionObserver observer : this.postulacionObservers) {
         observer.notifyPostulacion(postulante, this);
    }
}

>>TareaOperativa
public void validarPostulante(Postulante postulante) {
    if (postulante.tieneActividadesEntre(this.getFechaInicio(), this.getFechaFin()) {
        throw new BusinessException("El postulante ya participa de actividades de voluntariado entre " + this.getFechaInicio() + " y " + this.getFechaFin());
    }
}

>>TareaCoordinacion
public void validarPostulante(Postulante postulante) {
    if (!ModuloRRHH.getInstance().esJerarquico(postulante)) {  // o postulante.getLegajo()
        throw new BusinessException("El postulante no ocupa un puesto jerárquico");
    }
}

Decisiones que hay que cerrar:
  • cómo determino que una tarea está "planificada": si la colección de voluntarios es vacía
  • validación del postulante: si es manual no hay validación, cualquiera entra; si es automático hay que chequear que no hayamos alcanzado el máximo, si es semi-automático hay que delegar la validación al que corresponda
  • los observers lo dejamos para después
  • los métodos postulante.agregarTarea(this); y this.agregarPostulante(postulante); hacen el add de la colección, no hace falta codificarlos porque tenemos claro lo que hacen
  • saber si un postulante está asignado a otra tarea en un rango de fechas es fácil, si el postulante conoce a sus tareas. El resto es hacer la detección.
  • no puedo agregar la tarea en la colección de tareas del empleado porque todavía no se si lo voy a elegir como voluntario, eso queda para el caso de uso Cerrar inscripción

Codificamos algunos métodos de esta lista:

>>Tarea
public boolean isPlanificada() {
     return this.getVoluntarios().isEmpty();
}

>>PostulacionManual
public void validarPostulante(Postulante postulante, Tarea tarea) {
    // No hacemos nada
}

>>PostulacionAutomatica
public void validarPostulante(Postulante postulante, Tarea tarea) {
    if (tarea.maximoVoluntariosAlcanzado()) {
        throw new BusinessException("Se ha alcanzado el máximo de postulantes para esta tarea");
    }
}

Para sacar el máximo de voluntarios eso depende de si la tarea es operativa o de coordinación:

>>Tarea
public boolean maximoVoluntariosAlcanzado() {
     return this.getCupoMaximoVoluntarios() == this.getCantidadPostulantes();
}

public int getCantidadPostulantes() {
     return this.getPostulantes().size();
}

>>TareaOperativa
public boolean getCupoMaximoVoluntarios() {
     return this.cupoMaximoVoluntarios;
}

>>TareaCoordinacion
public int getCupoMaximoVoluntarios() {
    return 2;  // o una variable estática o una constante... return MaximoVoluntarios; que tenga un setter static
}

Ahora sí, nos concentramos en los observers: además de los obvios (mandar mails por cupo máximo y mínimo, etc. etc.), algunos podrían considerar que podría cerrarse la inscripción de voluntarios de la tarea, pasándola a estado "asignado", etc. Eso es válido, pero para asegurarse la consistencia del sistema tenemos que pensar también en el caso de uso que corre automáticamente. ¿Por qué? porque no alcanza con cerrar la inscripción, el caso de uso que corre automáticamente cada x tiempo es el que tiene que verificar que se hayan cubierto los cupos mínimos, si no la tarea nunca pasa a estado "cancelado" (o sea, si no quedarían eternamente planificadas las tareas automáticas).
Codificamos algunos observers:
>>CupoMinimoPostulacionObserver
public void notifyPostulacion(Postulante postulante, Tarea tarea) {
    if (tarea.cubreMinimoPostulantes()) {
        mailSender.sendMail(MAIL_FROM, tarea.getResponsable().getEMail(), "Postulación a tarea " + tarea, "Se ha alcanzado el mínimo de voluntarios para dicha tarea");
    }
}

>>Tarea
public boolean cubreMinimoPostulantes() {
    return this.getCantidadPostulantes() >= this.getCupoMinimoVoluntarios();
}

>>TareaOperativa
public int getCupoMinimoVoluntarios() {
    return this.cupoMinimoVoluntarios;
}

>>TareaCoordinacion
public int getCupoMinimoVoluntarios() {
    return 2;     // si se repite... mejor ponerlo en una constante como se indicaba arriba
}

Importante es delegar la pregunta en la tarea, para favorecer el lema "Tell, don't ask".
En el caso del cupo máximo, el método validarPostulante() de PostulacionAutomatica ya nos garantiza que a partir de aquí no se admiten nuevas postulaciones. Así que no hace falta subclasificar de PostulacionObserver, salvo que queramos enviar el mensaje cerrar() a la tarea en cuestión.
>>AuditarSectorPostulacionObserver
public void notifyPostulacion(Postulante postulante, Tarea tarea) {
    if (this.sector.equals(postulante.getSector())) {
         FileLogger.getInstance(this.getFileName()).log(...);
    }
}

Cierre de inscripción de postulantes y asignación de voluntarios

Este proceso puede dispararse

  • en forma automática, se dispara cada x tiempo
  • en forma manual, cuando el responsable de la tarea lo solicita

Independientemente de cómo se origine, lo que tiene que hacer el sistema es lo mismo:
Esto significa enviar el mensaje cerrar() a la tarea.

  • Si la tarea tiene suficientes postulantes, pasa a estado asignada
  • Si no, debe quedar cancelada

La solución admite múltiples formas de resolverlo, lo cierto es que necesitamos reflejar la lista de voluntarios vs. la lista de postulantes, y quizás un flag cancelado nos alcanza, pero debemos revisar la definición anterior del método isPlanificada()…

>>Tarea
public void cerrar() {
    if (this.cubreMinimoPostulantes()) {
        List<Postulante> postulantes = Collections.sort(this.getPostulantes(), this.tipoPostulacion.getOrdenPostulantes());
        this.voluntarios = postulantes.sublist(0, this.getCupoMaximoVoluntarios() - 1);
        // Ahora sí, relacionamos la tarea con los postulantes elegidos
        for (Postulante postulante : this.voluntarios) {
             postulante.agregarTarea(this);
        }
    } else {
        this.cancelar();
    }
}

public void cancelar() {
    this.cancelada = true;
}

// Modificamos el método isPlanificada()
public boolean isPlanificada() {
    return this.getVoluntarios().isEmpty() && !this.isCancelada();
}

El ordenamiento de los postulantes (que implica la posterior elección como voluntario) queda delegada en el Strategy:
>>PostulacionManual
public Comparator<Postulante> getOrdenPostulantes() {
    return new Comparator<Postulante>() {
             public int compare(Postulante p1, Postulante p2) {
                   return p1.getLegajo().compareTo(p2.getLegajo());
             }
        };
}

>>PostulacionAutomatica
public Comparator<Postulante> getOrdenPostulantes() {
    return null;
}

Claro, es un poco oscuro devolver null como Comparator para decir que deberíamos usar el "orden natural". Se puede escribir un Comparator default (p1.compareTo(p2));
Lo bueno es que la tarea hace tareas generales y lo que es específico trata de delegarlo al strategy del tipo de tratamiento que sea.
¿Cómo se implementa el tipo de postulación semi-automática? Es un Strategy que a su vez conoce a otro Strategy, de manera de poder delegar todas las preguntas en el real:
>>Postulacion SemiAutomatica
private TipoPostulacion criterio;

...

public void validarPostulante(Postulante postulante, Tarea tarea) {
    this.criterio.validarPostulante(postulante, tarea);
}

public Comparator<Postulante> getOrdenPostulantes() {
    return this.criterio.getOrdenPostulantes();
}

Que un tipo de postulación Semi-Automático no tenga a su vez n anidamientos de tipos de postulación semi-automáticos es un problema creacional que está fuera del alcance del enunciado.

Para el caso de uso de cierre automático esto comienza en la clase Empresa, que delega en cada proyecto el cierre de las tareas que correspondan:

>>Proyecto
public void cerrarTareas() {
    for (Tarea tarea : this.tareas) {  
         if (tarea.correspondeCerrar(new Date()) {
              tarea.cerrar();
         }
    }   
}

>>Tarea
public boolean correspondeCerrar(Date fechaDelDia) {
     Date fecha = fechaDelDia + 1;
     return (this.fechaInicio > fechaDelDia && this.isPlanificada());
}

Claro, no compila exactamente la operatoria con las fechas, pero se lo dejamos al lector utilizar Calendar, java.util.Date o un framework de fechas decente.
Lo importante es mostrar cómo funcionaría el caso de uso automático, que es simplemente delegar el cierre de inscripción a cada tarea seleccionada.

Seguimiento de las tareas

Para conocer el grado de avance de una tarea, esto nos remite al ejercicio de Manejo de Proyectos, podemos preguntarle a la tarea su % de avance.
Las implementaciones son diferentes según se trate de una tarea operativa (simple) o de coordinación (compuesta):

>>TareaOperativa
public double getPorcentajeAvance() {
    return this.porcentajeAvance;     // claro, es un getter 
}

>>TareaCoordinacion
public double getPorcentajeAvance() {
    double total = 0;
    for (Tarea tarea : this.getSubtareas()) {
         if (!tarea.isCancelada()) {
             total += tarea.getPorcentajeAvance();
         }
    }
    return total / this.getCantidadTareas();

}

El % de avance de un proyecto tiene un código similar a las de coordinación.
Para saber el % de tareas canceladas basta con seleccionar las tareas canceladas vs. total de las tareas, sabemos que se puede hacer sin mayor dificultad, no hace falta que lo codifiquemos.
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License