Estoy trabajando para refactorizar ciertos aspectos de un servicio web existente. La forma en que se implementan las API de servicio es mediante una especie de "canal de procesamiento", donde hay tareas que se realizan en secuencia. Como era de esperar, las tareas posteriores pueden necesitar información calculada por tareas anteriores, y actualmente la forma en que se hace esto es agregando campos a una clase de "estado de canalización".
He estado pensando (¿y esperando?) Que hay una mejor manera de compartir información entre los pasos de la tubería que tener un objeto de datos con un trillón de campos, algunos de los cuales tienen sentido para algunos pasos de procesamiento y no para otros. Sería un gran dolor hacer que esta clase sea segura para subprocesos (no sé si sería posible), no hay forma de razonar sobre sus invariantes (y es probable que no tenga ninguna).
Estaba hojeando el libro de patrones de diseño Gang of Four para encontrar algo de inspiración, pero no sentía que hubiera una solución allí (Memento tenía algo del mismo espíritu, pero no del todo). También busqué en línea, pero en el momento en que buscas "canalización" o "flujo de trabajo", te inunda la información de tuberías de Unix o los marcos y motores de flujo de trabajo patentados.
Mi pregunta es: ¿cómo abordaría el problema de registrar el estado de ejecución de una canalización de procesamiento de software, de modo que las tareas posteriores puedan utilizar la información calculada por las anteriores? Supongo que la principal diferencia con las tuberías Unix es que no solo te importa el resultado de la tarea inmediatamente anterior.
Según lo solicitado, algunos pseudocódigo para ilustrar mi caso de uso:
El objeto "contexto de canalización" tiene un montón de campos que los diferentes pasos de canalización pueden llenar / leer:
public class PipelineCtx {
... // fields
public Foo getFoo() { return this.foo; }
public void setFoo(Foo aFoo) { this.foo = aFoo; }
public Bar getBar() { return this.bar; }
public void setBar(Bar aBar) { this.bar = aBar; }
... // more methods
}
Cada uno de los pasos de la tubería también es un objeto:
public abstract class PipelineStep {
public abstract PipelineCtx doWork(PipelineCtx ctx);
}
public class BarStep extends PipelineStep {
@Override
public PipelineCtx doWork(PipelieCtx ctx) {
// do work based on the stuff in ctx
Bar theBar = ...; // compute it
ctx.setBar(theBar);
return ctx;
}
}
Del mismo modo para un hipotético FooStep
, que podría necesitar la barra calculada por BarStep antes, junto con otros datos. Y luego tenemos la llamada API real:
public class BlahOperation extends ProprietaryWebServiceApiBase {
public BlahResponse handle(BlahRequest request) {
PipelineCtx ctx = PipelineCtx.from(request);
// some steps happen here
// ...
BarStep barStep = new BarStep();
barStep.doWork(crx);
// some more steps maybe
// ...
FooStep fooStep = new FooStep();
fooStep.doWork(ctx);
// final steps ...
return BlahResponse.from(ctx);
}
}
fuente
Respuestas:
La razón principal para usar un diseño de tubería es que desea desacoplar las etapas. Ya sea porque una etapa se puede usar en varias canalizaciones (como las herramientas de shell de Unix) o porque obtiene algún beneficio de escala (es decir, puede pasar fácilmente de una arquitectura de nodo único a una arquitectura de nodo múltiple).
En cualquier caso, cada etapa de la tubería necesita recibir todo lo que necesita para hacer su trabajo. No hay ninguna razón por la que no pueda usar una tienda externa (por ejemplo, una base de datos), pero en la mayoría de los casos es mejor pasar los datos de una etapa a otra.
Sin embargo, eso no significa que deba o deba pasar un objeto de mensaje grande con cada campo posible (aunque vea a continuación). En cambio, cada etapa en la tubería debe definir interfaces para sus mensajes de entrada y salida, que identifican solo los datos que necesita la etapa.
Entonces tiene mucha flexibilidad en cómo implementar sus objetos de mensaje reales. Un enfoque es utilizar un gran objeto de datos que implemente todas las interfaces necesarias. Otra es crear clases de envoltura alrededor de un simple
Map
. Otra más es crear una clase de contenedor alrededor de una base de datos.fuente
Hay algunos pensamientos que me vienen a la mente, el primero es que no tengo suficiente información.
Las respuestas probablemente me harían pensar más cuidadosamente sobre el diseño, sin embargo, según lo que usted dijo, hay dos enfoques que probablemente consideraría primero.
Estructura cada etapa como su propio objeto. La enésima etapa tendría 1 a n-1 etapas como una lista de delegados. Cada etapa encapsula los datos y el procesamiento de los datos; reduciendo la complejidad general y los campos dentro de cada objeto. También puede hacer que las etapas posteriores accedan a los datos según sea necesario desde etapas mucho más antiguas atravesando a los delegados. Todavía tiene un acoplamiento bastante estrecho en todos los objetos porque son los resultados de las etapas (es decir, todos los atributos) lo que es importante, pero se reduce significativamente y cada etapa / objeto es probablemente más legible y comprensible. Puede hacer que sea seguro para subprocesos al hacer que la lista de delegados sea perezosa y usar una cola segura para subprocesos para completar la lista de delegados en cada objeto según sea necesario.
Alternativamente, probablemente haría algo similar a lo que estás haciendo. Un objeto de datos masivo que pasa por funciones que representan cada etapa. Esto es a menudo mucho más rápido y liviano, pero más complejo y propenso a errores debido a que es solo una gran pila de atributos de datos. Obviamente no es seguro para subprocesos.
Honestamente, hice el último más a menudo para ETL y algunos otros problemas similares. Estaba enfocado en el rendimiento debido a la cantidad de datos en lugar de la mantenibilidad. Además, eran únicos que no se usarían nuevamente.
fuente
Esto se parece a un patrón de cadena en GoF.
Un buen punto de partida sería observar lo que hace la cadena de bienes comunes .
fuente
Una primera solución que puedo imaginar es hacer explícitos los pasos. Cada uno de ellos se convierte en un objeto capaz de procesar un dato y transmitirlo al siguiente objeto de proceso. Cada proceso produce un nuevo producto (idealmente inmutable), de modo que no hay interacción entre los procesos y luego no hay riesgo debido al intercambio de datos. Si algunos procesos consumen más tiempo que otros, puede colocar un búfer entre dos procesos. Si explota correctamente un planificador para el subprocesamiento múltiple, asignará más recursos para vaciar los búferes.
Una segunda solución podría ser pensar "mensaje" en lugar de canalización, posiblemente con un marco dedicado. Luego, algunos "actores" reciben mensajes de otros actores y envían otros mensajes a otros actores. Organiza a sus actores en una tubería y entrega sus datos primarios a un primer actor que inicia la cadena. No hay intercambio de datos ya que el intercambio se reemplaza por el envío de mensajes. Sé que el modelo de actor de Scala se puede usar en Java, ya que aquí no hay nada específico de Scala, pero nunca lo he usado en un programa Java.
Las soluciones son similares y puede implementar la segunda con la primera. Básicamente, los conceptos principales son tratar con datos inmutables para evitar los problemas tradicionales debido al intercambio de datos y crear entidades explícitas e independientes que representen los procesos en su tubería. Si cumple con estas condiciones, puede crear fácilmente tuberías claras y simples y utilizarlas en un programa paralelo.
fuente