Script Ejercicio Lista de Correo

a) ¿Qué objetos candidatos encuentran?
b) ¿Cuáles son los casos de uso que propone el enunciado?
c) ¿Qué objetos son los responsables de resolver cada caso de uso?
d) Para cada caso de uso, piensen cómo lo resolverían (lo mejor es tirar un poco de código, es más fácil que escribirlo con palabras)
e) Armen un diagrama de clases tentativo

a) Objetos candidatos

  • ListaCorreo, que puede ser de suscripción
    • abierta
    • cerrada
  • y la forma de envío de mails
  • Usuario
  • Correo
  • componente que envía mails

b) Casos de uso

  1. Suscribir un nuevo usr a la lista
  2. Enviar un mail

Codificación de los casos de uso

Suscripción de un usuario nuevo

1) Suscripción: al objeto ListaCorreo le envío un

public void suscribir(un objeto Usuario)?

public void suscribir(datos del usuario por separado: n strings)?

El que no puede recibir el mensaje es el usuario, claramente.

Pero en este punto se plantean dos temas interesantes:

1) si armo o no el objeto usuario, quién y cómo dispara el mensaje.
Podemos hablar de que existe una UI, y que esa UI puede crear un
objeto Usuario, sin necesidad de trabajarlo por separado en una estructura
que sería incómoda:

public void suscribir(String nombre, String nick, String eMail, ...etc...)

2) si el tipo de suscripción a la lista es cerrada, tiene que haber una aprobación por parte de un administrador.
La tendencia común de los alumnos es a hacerlo en forma sincrónica:

  • se quiere mandar un flag "ok del administrador"
  • o bien se pretende detener el flujo del programa en espera de una confirmación.

Pero este problema hay que pensarlo en forma asincrónica, o sea:
momento a) el usuario pide suscribirse a la lista, el sistema procesa el pedido como pendiente
momento b) el administrador ingresa al caso de uso Miembros pendientes y confirma/rechaza al usuario

Corolario: ¡los casos de uso son 3!

Nuestras opciones pueden ser varias:
1) tener tres colecciones de usuarios: en espera, los confirmados y los rechazados
2) tener dos listas de usuarios: en espera y confirmados
3) tener una única lista con estados.

La opción 3 es incómoda para el resto de la operatoria del sistema, porque siempre tenemos que filtrar
los que están en espera o rechazados. Igual no es grave, basta con tener 3 métodos: getUsuariosActivos(),
getUsuariosRechazados(), getUsuariosEnEspera().

Cómo manejar los tipos de suscripción

  • un booleano: es lo más simple y además permite que una lista pase de tener suscripción abierta a cerrada y viceversa en forma muy simple. La contra: no escala
  • subclasificando las listas de correo en abiertas y cerradas: permite escalar pero impide subclasificar luego los tipos de envío de mails de la lista (recordar que la herencia trabaja con una única taxonomía)
  • tenemos un strategy: es sobrediseñar (al fin y al cabo, son dos tipos de suscripción nomás), pero permite reificar la idea de cómo se suscriben y es una solución que escala

Vamos a trabajarlo con un strategy no porque nos parezca la mejor opción (de hecho el boolean está justificado para mantenerlo lo más simple posible), sino por fines didácticos:

>>ListaCorreo
public void suscribir(Usuario usuario) {
   this.tipoSuscripcion.suscribir(this, usuario);
}

Claro, el Strategy tiene que conocer a la lista de correo o bien se la paso como parámetro.
Se hace un repaso comparativo entre Strategy stateless / stateful (el stateful requiere tener un objeto strategy por cada context,
por otro lado en el strategy stateless se altera la firma de los métodos):

>>SuscripcionAbierta
public void suscribir(ListaCorreo lista, Usuario usuario) {
    lista.agregarUsuario(usuario);
}

>>SuscripcionCerrada
public void suscribir(ListaCorreo lista, Usuario usuario) {
    lista.agregarUsuarioPendiente(usuario);
}

Aprobar usuarios pendientes de suscripción

A su vez este caso de uso puede definir un:

>>ListaCorreo
public void confirmar(Usuario usuario) {
   this.agregarUsuario(usuario);
}

Otra opción podría ser delegar la confirmación en cada strategy, porque si la lista es abierta, no tiene sentido confirmar al usuario (es una acción inválida para el negocio):

>>ListaCorreo
public void confirmar(Usuario usuario) {
   this.tipoSuscripcion.confirmar(this, usuario);
}

>>SuscripcionAbierta
public void confirmar(ListaCorreo lista, Usuario usuario) {
    throw new BusinessException("La lista de suscripción abierta no necesita confirmar usuarios pendientes");
}

>>SuscripcionCerrada
public void confirmar(ListaCorreo lista, Usuario usuario) {
   lista.agregarUsuario(usuario);
}

También podríamos validar que el usuario esté en la lista de pendientes y que no esté en la lista de usuarios activos:

>>SuscripcionCerrada
public void confirmar(ListaCorreo lista, Usuario usuario) {
   if (lista.getUsuariosActivos().contains(usuario)) {
      throw new BusinessException("El usuario " + usuario + " ya fue dado de alta");
   }
   if (!lista.getUsuariosPendientes().contains(usuario)) {
      throw new BusinessException("El usuario " + usuario + " no figura entre los usuarios pendientes");
   }   
   lista.agregarUsuario(usuario);
}

Esto da para pensar dos cosas:

  1. Si la interfaz de usuario está bien pensada, la pantalla te guía para que la operatoria sea la correcta. Entonces: a) en el caso de uso "Miembros pendientes" aparece una lista con los usuarios pendientes solamente, b) el administrador sólo podría seleccionar un usuario de esta lista y entonces la validación estaría de más
  2. Por otra parte, si dos administradores quieren confirmar un usuario pendiente al mismo tiempo, uno de los dos debería recibir el mensaje de error (en el apunte de concurrencia se trabaja sobre sincronización de procesos paralelos)

Repasamos que los errores de negocio se manejan con excepciones y no con códigos de error integers.

Definiendo una interfaz de salida

Repasemos el código que escribimos para los grupos de suscripción cerrada:

>>SuscripcionCerrada
public void suscribir(ListaCorreo lista, Usuario usuario) {
    lista.agregarUsuarioPendiente(usuario);
}

Sería bueno enviar un mail a los administradores del grupo avisándoles que hay un usuario que quiere suscribirse.
Esto nos lleva a pensar en una interfaz con un sistema externo: hay un componente que no forma parte de nuestro
sistema y necesitamos hablar con él. ¿Cómo hacemos?

Pensamos en objetos: hay varias APIs de Java que mandan mails (JavaMail, JMail, entre otros), más allá de que
el código que se termine ejecutando esté o no implementado en objetos.

¿Qué necesitamos para mandar un mail?

  • el o los mails de destino (en nuestro caso los administradores)
  • el mail origen (un mailer-daemon) (1)
  • un subject / asunto
  • un body: cuerpo

(1) contamos la idea de daemon
Aplicación que está permanentemente en estado de alerta en un servidor con el fin de realizar determinadas
tareas como, por ejemplo, enviar un mensaje de correo electrónico o servir una página web.

Una vez que tenemos los parámetros de la operación, podemos utilizar una interfaz Java para no tener que
atarnos a una tecnología específica, sabemos que de alguna manera se adaptará estos parámetros al formato
que la API de envío de mails necesite.

Dibujamos la interfaz Notificador, con un método

public void sendMail(String origen, String destino, String subject, String body);

Y para enviar el mail, vamos por la opción más simple, modificamos el método suscribir:

>>SuscripcionCerrada
public void suscribir(ListaCorreo lista, Usuario usuario) {
    lista.agregarUsuarioPendiente(usuario);
    notificador.sendMail(MAILER_DAEMON, lista.getMailAdministradores(), "Usuario pendiente", "El usuario " + usuario + " ha solicitado la inscripción a la lista " + lista);
}

¿Qué pasa si falla la notificación?
Esperemos que se dispare una excepción. O sea, no queremos recibir un -1, en todo caso el que implemente la clase concreta
adaptará el -1 a un new SystemException("El envío de mails falló").

Por otra parte: "vamos a ignorar los mails que no se pudieron enviar" dice el enunciado.
Pero de esta manera no estamos ignorando los errores, el usuario va a recibir el Stack Trace por pantalla,
para ignorarlo hacemos:

>>SuscripcionCerrada
public void suscribir(ListaCorreo lista, Usuario usuario) {
    lista.agregarUsuarioPendiente(usuario);
    try {
        notificador.sendMail(MAILER_DAEMON, lista.getMailAdministradores(), "Usuario pendiente", "El usuario " + usuario + " ha solicitado la inscripción a la lista " + lista);
    } catch (SystemException e) {
        // No hacemos nada según pide el negocio
    }
}

Por otra parte, ¿de dónde sale notificador?
¿Es una variable de instancia de Notificador?
Mmm… no pareciera una decisión fácil de tomar, porque no depende

  • ni del usuario
  • ni de la lista
  • ni del tipo de suscripción de la lista.

Entonces… ¿dónde lo ponemos?

Singleton

Toda la aplicación de Listas de correo va a compartir la misma forma de notificar.
Entonces una posibilidad concreta es definir un único objeto (una única instancia) al cual se pueda acceder desde cualquier punto.
Por eso vamos a utilizar un patrón creacional que se llama Singleton:

  • tendremos un método estático (de clase) para acceder a la única instancia
>>SuscripcionCerrada
public void suscribir(ListaCorreo lista, Usuario usuario) {
    lista.agregarUsuarioPendiente(usuario);
    try {
        NotificadorPorMail.getInstance().sendMail(MAILER_DAEMON, lista.getMailAdministradores(), "Usuario pendiente", "El usuario " + usuario + " ha solicitado la inscripción a la lista " + lista);
    } catch (SystemException e) {
        // No hacemos nada según pide el negocio
    }
}

¿Cómo implementamos la clase NotificadorPorMail?

public class NotificadorPorMail() {

    ...

    private NotificadorPorMail() {
    }

    private static Notificador instance;

    public static Notificador getInstance() {
        if (instance == null) {
            instance = new NotificadorPorMail();
        }
        return instance;
    }

}

Esto nos asegura tener una sola instancia … por VM (claro, si tenemos clusters de app server tendremos un único objeto por
ambiente). El constructor por default queda privado, esto significa que si quisiera hacer

>>SuscripcionCerrada
public void suscribir(ListaCorreo lista, Usuario usuario) {
    lista.agregarUsuarioPendiente(usuario);
    try {
        // ERROR
        new NotificadorPorMail().sendMail(MAILER_DAEMON, lista.getMailAdministradores(), "Usuario pendiente", "El usuario " + usuario + " ha solicitado la inscripción a la lista " + lista);
        //
    } catch (SystemException e) {
        // No hacemos nada según pide el negocio
    }
}

el new NotificadorPorMail() no compila.

El Singleton nos permite no tener que preocuparnos por saber quién conoce al notificador, esa única instancia es accesible
a partir de la clase concreta NotificadorPorMail (ojo, el objetivo del Singleton no es solamente que haya una sola instancia
de una clase, sino también proporcionar un punto de acceso global para que cualquier objeto le pueda mandar mensajes).

En la introducción del concepto parece que es todo ganancia, sin embargo cabe mencionar que los singletons complican los tests unitarios: si bien testear la suscripción de un usuario a un grupo cerrado es en sí un test de integración, tampoco es trivial poder testear la suscripción de un grupo abierto, porque no es fácil reemplazar el NotificadorPorMail por un mock (un objeto dummy que no mande mails ni haga nada) sin tener que toquetear el método suscribir()

Observers en lugar de Singletons

Otra opción en lugar de usar directamente una clase singleton es simular un "twitter" entre dos actores:

  • un objeto de interés o subject: en nuestro caso sería la lista de correo de tipo cerrada
  • un interesado en el objeto u observador: podemos generar una interfaz IObserver, con una clase concreta NotificadorPorMailObserver que implemente dicha interfaz.

Cada lista de correo conoce n observers, que pueden ser:

  • un observer por cada administrador de la lista (cuando se genera un admin se agrega también un observer y cuando se da de baja al admin también se elimina el observer)
  • un observer único para que mande mails a todos los administradores de la lista y otros observers posibles que hagan otras cosas: grabar un archivo de log, guardar info en una base de datos, actualizar una pantalla de monitoreo, etc.

Pero si queremos pensar en que a los observers les interesa un determinado evento, tenemos que cambiar la firma del método para no hacerlo tan "mail-dependiente", es decir, que la interfaz del observer no esté tan orientada a mandar un mail:

>>SuscripcionCerrada
public void suscribir(ListaCorreo lista, Usuario usuario) {
    lista.agregarUsuarioPendiente(usuario);
}

public void agregarUsuarioPendiente(Usuario usr) {
   this.usuariosPendientes.add(usr);
   for (Observer observer : this.getObservers()) {
        observer.notifyUsuarioPendiente(this, usr);
   }
}

>>NotificadorJavaMailObserver (vemos una implementación sólo por querer hacerlo, no es obligatorio para Algo2)
public void notifyUsuarioPendiente(ListaCorreo lista, Usuario usr) {
    for (Usuario admin : lista.getAdministradores()) {
        Properties props = System.getProperties();
        props.put("mail.smtp.host", smtpServer);
        Session session = Session.getDefaultInstance(props, null);
        Message msg = new MimeMessage(session);
        msg.setFrom(new InternetAddress("admin@mygroups.com"));
        msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse(admin.getMail(), false));
        msg.setSubject("Usuario solicita incorporarse al grupo");
        msg.setText("El usuario " + usr + " ha solicitado incorporarse a la lista " + lista);
        Transport.send(msg);
    }
}

Enviar un correo

El enunciado dice: "Enviar un correo, recibiendo la dirección de e-mail origen del correo, título y texto."
¿Quién envía el mail? El usuario, pero en este caso vamos a necesitar trabajar con una interfaz entrante que transforme la recepción de un mail
en un mensaje a un objeto.
Ahora, ¿a qué objeto?

  • podría ser al usuario
  • o bien a la lista de correo
>>ListaCorreo
public void enviarCorreo(Usuario origen, String titulo, String texto) {
    ...
}

¿Se puede abstraer los parámetros? Sí, podríamos generar un objeto que agrupa información pero que
además le da un nombre a eso que estamos pasando: Mail o Mensaje no suena mal. Otra ventaja asociada es que
cualquier agregado o modificación de parámetros no impacta la firma del método.
>>ListaCorreo
public void enviarCorreo(Mensaje mensaje) {
    ...
}

Nota del docente: técnicamente no se si llamarlo Value Object y tampoco estoy seguro de querer catalogarlo. Se qué quiero tener
un objeto y por qué lo quiero (lo expliqué arriba). Para más información ver http://c2.com/cgi/wiki?BusinessObject

Revisamos un poco más el enunciado y dice: "El envío de correos a la lista puede definirse como abierto (cualquiera
puede enviar correos a la lista) o restringido a los miembros de la lista."
Entonces vamos a delegar en un strategy el envío de mails a la lista (una vez más, podríamos haber utilizado un booleano o bien subclasificar ListaAbierta o ListaRestringidaAMiembros):

>>ListaCorreo
public void enviarCorreo(Mensaje mensaje) {
    tipoEnvioMails.enviarCorreo(this, mensaje);
}

Para el tipo de envío abierto, la codificación es sencilla:

public void enviarCorreo(ListaCorreo lista, Mensaje mensaje) {
    try {
        NotificadorPorMail.getInstance().sendMail(MAILER_DAEMON, lista.getMailsUsuariosActivos(), mensaje.getTitulo(), mensaje.getTexto());
    } catch (SystemException e) {
        // No hacemos nada según pide el negocio
    }
}

Para el tipo de envío restringido a la lista, hacemos:

public void enviarCorreo(ListaCorreo lista, Mensaje mensaje) {
    try {
        if (lista.usuarioRegistrado(mensaje.getUsuario())) {
           NotificadorPorMail.getInstance().sendMail(MAILER_DAEMON, lista.getMailsUsuariosActivos(), mensaje.getTitulo(), mensaje.getTexto());
        } else {
           throw new BusinessException("La lista sólo permite enviar mails a los miembros registrados");
        }
    } catch (SystemException e) {
        // No hacemos nada según pide el negocio
    }
}

Un pequeño refactor

Si analizamos el código de ambos tipos de envío, la idea de mandar el mail nos quedó duplicada en cada strategy.
En realidad, lo que cada strategy hace es validar que el mail pueda enviarse. Entonces por qué no separar:
a) la validación
b) el envío propiamente dicho.
Refactorizando nos queda:

>>ListaCorreo
public void enviarCorreo(Mensaje mensaje) {
    tipoEnvioMails.validarEnvio(this, mensaje);
    try {
        NotificadorPorMail.getInstance().sendMail(MAILER_DAEMON, lista.getMailsUsuariosActivos(), mensaje.getTitulo(), mensaje.getTexto());
    } catch (SystemException e) {
        // No hacemos nada según pide el negocio
    }
}

>>EnvioAbierto
public void validarEnvio(ListaCorreo lista, Mensaje mensaje) {
}

>>EnvioRestringidoAMiembros
public void validarEnvio(ListaCorreo lista, Mensaje mensaje) {
    if (!lista.usuarioRegistrado(mensaje.getUsuario())) {
       throw new BusinessException("La lista sólo permite enviar mails a los miembros registrados");
    }
}

Por último, "Cada usuario puede tener definida más de una dirección de e-mail, desde las que puede
enviar mensajes a la(s) lista(s). De todas las direcciones de e-mail que tenga, una es a la
que se le envían los mails."
Esto ¿cómo afecta a la aplicación?
Simplemente para resolver el método getMailsUsuariosActivos() y getMailsAdministradores() cada usuario tiene un mail principal y una lista de mails alternativos.

La última decisión es cómo detectar los administadores:

  1. la clase Usuario tiene un flag administrador
  2. la clase Usuario tiene un perfil y el perfil sabe si es administrador
  3. la lista de correo maneja una lista de administradores aparte de la de usuarios, esto obliga a incluirlo en la lista de usuarios activos; podemos tener un esquema redundante (un usuario está en la lista de usuarios y administradores a la vez, lo cual puede resultar confuso y además error-prone)

La alternativa más simple es la primera: tener un flag administrador para usuario logra el menor impacto posible

  • el método getUsuarios() de la lista no necesita tener nada en cuenta
  • el método getAdministradores() de la lista filtra aquellos que tengan el flag administrador = true
  • y después podemos tener un método que dado una lista de usuarios, nos devuelva la lista de mails principales

Cosas que quedaron en el tintero

Si tengo que pensar en el testeo unitario, ¿qué dificultades encuentro?
Cuando tengo que interactuar con un sistema externo, pierdo la unitariedad.
Más allá de eso, dejamos algunos apuntes:

  1. Hacer un testCase del envío de mails y probar los cuatro casos abierta/cerrada, miembro/no miembro.
  2. La pregunta sería cómo testear que el mail se envió, entonces poner un usuario mock entre los usuarios de la lista. A ese usuario después le puedo preguntar si a él le llegó el mail.
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License