Script Auxilio Mecánico - una posible solución

Enunciado

Auxilio Mecánico

A continuación vamos a exponer una posible solución, no es la única forma de resolverlo ni es la solución que la cátedra quiere que piensen, pero sí aparecen guías que pueden ser útiles para encarar un examen.

Casos de uso

  • Generar un pedido
    • Validar que el pedido cumple restricciones
      • Servicio válido en base al tipo de cliente
      • La deuda del cliente en base al tipo de cliente
    • Asignar el tipo de vehículo en base al servicio
      • Conocer los vehículos disponibles
      • Ordenarlos por prioridad
  • Informar la finalización de un servicio (cerrar)
    • Actualizar el estado del pedido
    • Disparar notificaciones y registros de tiempos promedio

Generar un pedido

Preguntas a hacernos:

  • ¿recibo un pedido, la estructura por separado o qué?
  • ¿quién es el objeto responsable? ¿puede ser el objeto Pedido? ¿puedo mandar un mensaje a la clase Pedido?

Opciones posibles:

  • no recibir un pedido, sino un PedidoBuilder, que termina de generarlo
  • recibir el Pedido al que todavía le falta asignarle el vehículo (el Pedido como viene está inconcluso)
  • armar otro objeto que modele el Pedido como parámetro (un PedidoParameter) no resulta del todo convincente, ya existe para eso una abstracción que es el Pedido.

El Pedido es lo más simple, el PedidoBuilder puede considerarse en cierta medida algo de sobrediseño, pero la justificación va por el lado de que en la interfaz estamos siendo consistentes en que todavía no tenemos un pedido hasta que no se termina de validar y asignar el móvil.

¿Qué objeto es el responsable?

  • puede ser un objeto Empresa, lo que no tiene que pasar es que la empresa conozca todo de todos
  • puede ser un objeto que modele el caso de uso GenerarPedido
  • que el Pedido tenga un método de clase generarPedido no es recomendable porque veremos que la responsabilidad incluye conocer a todos los pedidos, y eso obligaría a tener una variable de clase (static) pedidos en Pedido, y se nos dificulta utilizar el mecanismo de herencia para atributos o métodos de clase.

Codificamos un poco el método (en la clase Empresa), el Pedido viene con la siguiente información cargada:

  • fecha (veremos más abajo por qué)
  • cliente o vehículo (en el enunciado es lo mismo)
  • el domicilio donde se encuentra
  • la descripción del problema
  • tipo de reparación (compleja o simple), se puede modelar con un flag o bien pasar un objeto que implemente una interfaz
  • si requiere remolque o no, esto seguro se modela con un flag
public void generarPedido(Pedido pedido) {
     pedido.validar();
     this.asignarMovil(pedido);
     this.agregarPedido(pedido);
}

public void asignarMovil(Pedido pedido) {
     List moviles = this.getMovilesDisponiblesPara(pedido);
     Collection movilesOrdenados = Collections.sort(moviles);
     Movil movil = moviles.get(0);
     if (movil == null) {
         throw new BusinessException("No existen móviles disponibles para el pedido");
     }
     pedido.asignarMovil(movil);
}

Nos concentramos en las validaciones, desde la clase Pedido:
public void validar() {
     cliente.validar(pedido);
}

Tipos de cliente

Podemos tener un string/int (0 = Platinum, 1 = Classic, 2 = Economic), subclasificar los tipos de cliente en Platinum, Classic y Economic o ir por un Strategy que permita una cierta flexibilidad. ¿Qué dice el enunciado?
"Interesa para el sistema poder cambiar estas restricciones a futuro o generar nuevas"
Ok, entonces subclasificar implica una complejidad mayor, el int no escala a futuro con nuevas categorías, el strategy permite reificar el Tipo de cliente e intercambiar la forma en que valido la deuda y el servicio.
Implementamos la validación desde el punto de vista del cliente:

public void validar(Pedido pedido) {
     tipoCliente.validarDeuda();
     tipoCliente.validarServicio(pedido);
}

La implementación del validar deuda en realidad requiere definir una interfaz con un sistema externo, que en definitiva es enviar un mensaje a un objeto.
Consideremos que ese objeto es un Singleton y que trabaja en forma sincrónica, esto es: el envío del mensaje preguntando la deuda se ejecuta y vuelve el flujo inmediatamente hacia nosotros, esto es claramente más simple que hacerlo asincrónico (esperar que nos conteste en otro momento):
public void validar() {
     BigDecimal deuda = ModuloPagos.getInstance().getDeuda(this);
     tipoCliente.validarDeuda(deuda);
     tipoCliente.validarServicio(pedido);
}

Opciones para codificar la interfaz del módulo de pagos:
  • la que hicimos fue tener el Singleton con un método estático getInstance() que nos garantiza un único punto de acceso en todo el sistema, el método getDeuda(Cliente cliente) es de instancia, no es estático
  • otra alternativa es tener un método estático getDeuda(Cliente cliente). Eso puede ser una desventaja si neesitamos que el Singleton tenga estado, porque un método estático no puede acceder a las variables de instancia de una clase
  • también podríamos tener una variabe moduloPagos, e instanciarlo a través de un Singleton. El método quedaría:
public void validar() {
     BigDecimal deuda = moduloPagos.getDeuda(this);
     tipoCliente.validarDeuda(deuda);
     tipoCliente.validarServicio(pedido);
}

Esto tiene como ventaja que resultaría fácil pasar de un Singleton a tener muchas instancias o viceversa.

Codificamos el método validarDeuda en Economic:

public void validarDeuda() {
    if (deuda.floatValue() > 0) {
       throw new BusinessException("El cliente excede el límite de deuda, no puede efectuar pedidos");
    }
}

Lo mismo para los Platinum:
public void validarDeuda() {
    if (deuda.floatValue() > this.cliente.getMontoCuota().floatValue()) {
       throw new BusinessException("El cliente excede el límite de deuda, no puede efectuar pedidos");
    }
}

Ah, pero entonces el código que se repite puede estar en cliente:
public void validar() {
     BigDecimal deuda = ModuloPagos.getDeuda(this);
     if (tipoCliente.excedeDeuda()) {
         throw new BusinessException("El cliente excede el límite de deuda, no puede efectuar pedidos");
     }
     if (!tipoCliente.cumpleServicio(pedido)) {
         throw new BusinessException("El cliente no puede recibir este servicio");
     }
}

>>TipoCliente
public abstract boolean excedeDeuda();

>>Economic
public boolean excedeDeuda() {
    return (deuda.floatValue() > 0);
}

>>Platinum
public boolean excedeDeuda() {
    return (deuda.floatValue() > this.cliente.getMontoCuota().floatValue());
}

El lector puede continuar con la validación restante.
Respecto a la validación de los tipos de servicio, también distinguimos por tipo de cliente:
>>Economic
public boolean cumpleServicio(Pedido pedido) {
    ...
}

>>Platinum
public boolean cumpleServicio(Pedido pedido) {
    return true;
}

Mmm… revisemos nuevamente la validación del Economic:
"hasta 5 reparaciones al año sin remolques ni reparaciones complejas"
Entonces
  • necesitamos saber cuántas reparaciones en el año tuvo ese cliente. ¿Qué nos conviene? ¿Pedírselo a la empresa o que el propio cliente lo conozca? Lo segundo parece mejor, pero ojo que eso requiere asegurarme de que en algún momento el cliente conozca al pedido (salvo que asuma que eso viene dado antes de generar el pedido, hay que tener en cuenta que eso hay que escribirlo)
  • tenemos que saber que el pedido no pida remolques ni reparaciones complejas, si son dos flags es fácil
>>Economic
public boolean cumpleServicio(Pedido pedido) {
    int cantidadReparacionesAnio = this.cliente.getReparacionesDelAnio(Calendar.get(Calendar.YEAR));
    return (cantidadReparacionesAnio < 5) && (!pedido.isRemolque()) && (!pedido.isReparacionCompleja());
}

Lo único que nos falta definir es cómo distinguir del pedido si es:
  • reparación
  • o remolque

La respuesta más simple es que el flag ofrezca un método getter isRemolque() y la reparación es el opuesto a isRemolque():

>>Pedido
public boolean isRemolque() {
    return this.isRemolque;
}

public boolean isReparacion() {
    return !this.isRemolque();
}

Un pequeño refactor consiste en preguntarle al pedido directamente si se trata de una reparación simple:
>>Economic
public boolean cumpleServicio(Pedido pedido) {
    int cantidadReparacionesAnio = this.cliente.getReparacionesDelAnio(Calendar.get(Calendar.YEAR));
    return cantidadReparacionesAnio < 5 && pedido.isReparacionSimple();
}

>>Pedido
public boolean isReparacionSimple() {
    return this.isReparacion() && !this.isReparacionCompleja();
}

"Si alguna de estas cosas no se cumple, no se puede generar el pedido, utilice la herramienta que mejor crea conveniente para informar correctamente el motivo de la no aceptación del pedido."
¿Qué herramienta utilizamos?
Las excepciones, claro, en este caso no chequeadas.
Si fueran chequeadas, debemos respetar la cadena de throws. Esto va en el gusto de cada persona, el docente eligió no tener que modificar la firma de los métodos desarrollados.

¿Hace falta desarrollar el getReparacionesDelAnio(int) de Cliente?
En la opinión de quien escribe esto, no, porque sabemos cómo encararlo:

  • hay que buscar los pedidos que sean reparaciones (ya lo tenemos implementado en Pedido)
  • el pedido tiene una fecha (de cumplimiento o de creación)

simplemente tenemos que filtrar los pedidos que cumplan este criterio.

Sólo nos falta volver al método que calcula los móviles disponibles:

public void asignarMovil(Pedido pedido) {
     List moviles = this.getMovilesDisponiblesPara(pedido);
     Collection movilesOrdenados = Collections.sort(moviles);
     if (movilesOrdenados.isEmpty()) {
         throw new BusinessException("No existen móviles disponibles para el pedido");
     }
     Movil movil = moviles.get(0);
     pedido.asignarMovil(movil);
}

Vamos a separar las tareas:
  • por un lado validamos qué móviles son adecuados para un pedido (filtrado)
  • por otro tenemos que ordenarlos según el criterio que establece cada tipo de cliente (ordenamiento - posterior selección)

Pensemos el método getMovilesDisponiblesPara(Pedido), qué tiene que hacer:

  • la empresa conoce a todos los móviles
  • cada móvil tiene que responder si es adecuado para ese pedido (aquí no necesitamos dejar abierta la posibilidad de que una mini-grúa pase a ser gran-grúa, entonces en lugar de hacer un strategy podemos subclasificar MiniGrua, GranGrua y Minitaller de la superclase Movil y nos alcanza)

Codificamos un poco para tener en claro lo que dijimos arriba:

public List<Movil> getMovilesDisponiblesPara(Pedido pedido) {
    List<Movil> result = new ArrayList<Movil>();
    for (Movil movil : this.getMoviles()) {
        if (movil.isDisponiblePara(pedido)) {
            result.add(movil);
        }
    }
}

Vemos las implementaciones de la mini-grúa y minitaller:

  • El minitaller móvil sólo puede hacer reparaciones simples y que no requieran remolque.
  • La minigrúa puede remolcar vehículos de hasta 3 toneladas.
>>Minitaller
public boolean isDisponiblePara(Pedido pedido) {
    return pedido.isReparacionSimple();
} 

>>Minigrua
public boolean isDisponiblePara(Pedido pedido) {
    return !pedido().getVehiculo().esPesado();
} 

>>Pedido
public Vehiculo getVehiculo() {
    return this.cliente.getVehiculo();
}

>>Vehiculo
public boolean esPesado() {
    return this.peso > 3000;
}

Aquí pasan algunas cosas:
  1. Estamos violando la ley de Demeter, pero no tanto. Al pedido le pido el vehículo (eso me da la flexibilidad de cambiar el modelo el día de mañana y admitir que un cliente tenga n vehículos, pero el pedido sepa cuál vehículo es el que está asociado a él)
  2. El vehículo define si es "pesado" (más de 3 toneladas). Si la "pesadez" de un vehículo difiere en base a lo que cada móvil puede levantar, eso podría no estar tan bueno. Pero por el momento lo dejamos ahí.

Armamos un diagrama de clases que refleje el status de la app hasta este momento:

GenerarPedido.PNG
  1. Nos falta ver de qué manera ordenamos los móviles, esto depende tanto del móvil como del tipo de cliente…
public void asignarMovil(Pedido pedido) {
    List moviles = this.getMovilesDisponiblesPara(pedido);
    Collection movilesOrdenados = Collections.sort(moviles);
    if (movilesOrdenados.isEmpty()) {
        throw new BusinessException("No existen móviles disponibles para el pedido");
    }
    Movil movil = moviles.get(0);
    pedido.asignarMovil(movil);
}

Necesitamos hacer un cambio, vamos a ordenar según el criterio que establezca el cliente:
>>Empresa
public void asignarMovil(Pedido pedido) {
    List moviles = this.getMovilesDisponiblesPara(pedido);
    if (moviles.isEmpty()) {
        throw new BusinessException("No existen móviles disponibles para el pedido");
    }
    Collections.sort(moviles, pedido.getCliente().getMovilComparator());
    pedido.asignarMovil(moviles.get(0));
}

>>Pedido
public void asignarMovil(Movil movil) {
    this.movil = movil;
    movil.agregarPedido(this);
    //this.cliente.agregarPedido(this);  si no se hizo antes
}

Vamos a ver qué ofrece como interfaz Comparator: http://download.oracle.com/javase/1.4.2/docs/api/java/util/Comparator.html
… la posibilidad de decir cómo ordenar dos elementos (cuál es mayor que otro).
Entonces el Collections.sort ordena los elementos en base a la forma en que se comparan.
Sólo basta con definir el criterio implementando un Comparator<Movil> para cada tipo de cliente (el cliente delega en sus strategies):

>>Cliente
public Comparator<Movil> getMovilComparator() {
    return this.tipoCliente.getMovilComparator();
}

>>Platinum
public Comparator<Movil> getMovilComparator() {
    return new MenosPendienteComparator(); 
}

>>Economic / Classic
public Comparator<Movil> getMovilComparator() {
    return new MovilMasBaratoComparator(); 
}

Las clases MenosPendienteComparator y MovilMasBaratoComparator implementan Comparator<Movil>, de la siguiente manera:
>>MenosPendienteComparator
public int compare(Movil m1, Movil m2) {
    return m1.getCantidadPedidosPendientes().compareTo(m2.getCantidadPedidosPendientes());
}

Como cada móvil conoce sus pedidos pendientes, es simplemente filtrar de la lista de pedidos los que están pendientes (delegando la pregunta al pedido):
>>Movil
public int getCantidadPedidosPendientes() {
    return this.getPedidosPendientes().size();
}

public List<Movil> getPedidosPendientes() {
    List<Movil> result = new ArrayList<Movil>();
    for (Pedido pedido : this.pedidos) {
        if (pedido.isPendiente()) {
            result.add(pedido);
        }
    }
    return result;
}

Para elegir el móvil más barato, el Comparator delega la pregunta al móvil que le da un número de prioridad (a menor prioridad - más barato):
>>MovilMasBaratoComparator
public int compare(Movil m1, Movil m2) {
    return m1.getOrdenPrioridad().compareTo(m2.getOrdenPrioridad());
}

>>Minitaller
public int getOrdenPrioridad() {
    return 10;
}

>>Minigrua
public int getOrdenPrioridad() {
    return 20;
}

>>GranGrua
public int getOrdenPrioridad() {
    if (!this.tieneTallerAltaComplejidad) {
       return 30;
    } else {
       return 40;
    }
}

El diagrama de clase ilustra esta situación:

OrdenarMoviles.PNG

Cerrar un pedido

Esto ocurre cuando el móvil informa la finalización del servicio.
Revisamos lo que anotamos previamente:

  • Actualizar el estado del pedido
  • Disparar notificaciones y registros de tiempos promedio
>>Pedido
public void cerrar() {
    this.fechaFinalizacion = new Date();
    ...
    disparar notificaciones y actualizar tiempos promedio
    ...
}

Qué ideas de diseño tenemos:
  • hacer un if gigante preguntando ésto o aquello
  • tener una colección de observers o listeners, que escuchan cuando se cierra un pedido, cada observer hace algo distinto

La segunda opción es más flexible, permite agregar nuevas formas de notificación o de responder ante el evento "cerrar un pedido". ¿Qué dice el enunciado?
"A futuro interesaría que el sistema pueda incorporar fácilmente nuevas funcionalidades ante la finalización del servicio por parte de un móvil."
Entonces vamos a codificar la solución de los observers:

>>Pedido
public void cerrar() {
    this.fechaFinalizacion = new Date();   
    for (PedidoObserver pedidoObserver : this.pedidoObservers) {
        pedidoObserver.notifyCierre(this);
    }
}

>>AuditarReparaciones
public void notifyCierre(Pedido pedido) {
    if (pedido.isReparacion() && pedido.getCliente().esReciente()) {
       MailSender.getInstance().enviarMail(FROM_AUXILIO_MECANICO, this.casillaDestino, "Reparacion hecha", "El movil " + pedido.getMovil() + " hizo la reparación del pedido " + pedido);
    }
}

>>MovilesDemorones
public void notifyCierre(Pedido pedido) {
    if (pedido.getTiempoPromedio() > 3) {
       Logger.getInstance().loguear(this.nombreArchivo, "El tiempo promedio del móvil " + pedido.getMovil() + " para el pedido " + pedido + " excedió las 3 horas");
    }
}

El requerimiento "Registrar los tiempos promedios de servicio (medidos desde que se asigna el móvil hasta que éste informa el fin del servicio)" lo realiza el pedido al asignar la fecha de finalización.

Vemos el diagrama de clases para ver cómo se documentan los observers:

NotificacionPorCierre.PNG
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License