Buenas estrategias de implementación para encapsular datos compartidos en una tubería de software

13

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);
    }
}
RuslanD
fuente
66
hacer post no cruz sino bandera para un mod para mover
trinquete monstruo
1
Lo haré en el futuro, creo que debería pasar más tiempo familiarizándome con las reglas. ¡Gracias!
RuslanD
1
¿Está evitando el almacenamiento de datos persistente para su implementación, o hay algo en juego en este momento?
CokoBWare
1
Hola RuslanD y bienvenido! De hecho, esto es más adecuado para programadores que Stack Overflow, por lo que eliminamos la versión SO. Tenga en cuenta lo que @ratchetfreak mencionó, puede marcar la atención de moderación y solicitar que se migre una pregunta a un sitio más adecuado, sin necesidad de cruzar la publicación. La regla general para elegir entre los dos sitios es que los Programadores son para los problemas que enfrenta cuando está frente a la pizarra para diseñar sus proyectos, y Stack Overflow es para problemas más técnicos (por ejemplo, problemas de implementación). Para más detalles, consulte nuestras preguntas frecuentes .
yannis
1
Si cambia la arquitectura a un DAG de procesamiento (gráfico acíclico dirigido) en lugar de una tubería, puede pasar explícitamente los resultados de los pasos anteriores.
Patrick

Respuestas:

4

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.

parsifal
fuente
1

Hay algunos pensamientos que me vienen a la mente, el primero es que no tengo suficiente información.

  • ¿Cada paso produce datos utilizados más allá de la tubería, o solo nos interesan los resultados de la última etapa?
  • ¿Hay muchas preocupaciones de big data? es decir. problemas de memoria, problemas de velocidad, etc.

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.

dietbuddha
fuente
1

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 .

Una técnica popular para organizar la ejecución de flujos de procesamiento complejos es el patrón "Cadena de responsabilidad", como se describe (entre muchos otros lugares) en el clásico libro de patrones de diseño "Gang of Four". Aunque los contratos de API fundamentales necesarios para implementar este patrón de diseño son extremadamente simples, es útil tener una API base que facilite el uso del patrón y (lo que es más importante) aliente la composición de las implementaciones de comandos de múltiples fuentes diversas.

Con ese fin, la API de la cadena modela un cálculo como una serie de "comandos" que se pueden combinar en una "cadena". La API para un comando consta de un único método ( execute()), al que se le pasa un parámetro de "contexto" que contiene el estado dinámico del cálculo, y cuyo valor de retorno es un valor booleano que determina si el proceso para la cadena actual se ha completado o no ( verdadero), o si el procesamiento debe delegarse al siguiente comando en la cadena (falso).

La abstracción de "contexto" está diseñada para aislar las implementaciones de comandos del entorno en el que se ejecutan (como un comando que se puede utilizar en un Servlet o Portlet, sin estar directamente vinculado a los contratos API de ninguno de estos entornos). Para los comandos que necesitan asignar recursos antes de la delegación, y luego liberarlos al regresar (incluso si un comando delegado arroja una excepción), la extensión "filtro" para "comando" proporciona un postprocess()método para esta limpieza. Finalmente, los comandos se pueden almacenar y buscar en un "catálogo" para permitir el aplazamiento de la decisión sobre qué comando (o cadena) se ejecuta realmente.

Para maximizar la utilidad de las API de patrón de la Cadena de responsabilidad, los contratos de interfaz fundamentales se definen de una manera con cero dependencias distintas de un JDK apropiado. Se proporcionan implementaciones de clase base convenientes de estas API, así como implementaciones más especializadas (pero opcionales) para el entorno web (es decir, servlets y portlets).

Dado que las implementaciones de comandos están diseñadas para cumplir con estas recomendaciones, debería ser factible utilizar las API de la Cadena de responsabilidad en el "controlador frontal" de un marco de aplicación web (como Struts), pero también ser capaz de usarlo en el negocio niveles de lógica y persistencia para modelar requisitos computacionales complejos a través de la composición. Además, la separación de un cálculo en comandos discretos que operan en un contexto de propósito general permite la creación más fácil de comandos que son comprobables por unidad, porque el impacto de ejecutar un comando se puede medir directamente observando los cambios de estado correspondientes en el contexto que se proporciona ...

Aldrin Leal
fuente
0

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.

mgoeminne
fuente
Oye, actualicé mi pregunta con un pseudocódigo; de hecho, tenemos los pasos explícitos.
RuslanD