Buone strategie di implementazione per l'incapsulamento di dati condivisi in una pipeline software

12

Sto lavorando per ridisegnare alcuni aspetti di un servizio web esistente. Il modo in cui le API dei servizi vengono implementate consiste nel disporre di una sorta di "pipeline di elaborazione", in cui sono presenti attività eseguite in sequenza. Non sorprendentemente, le attività successive potrebbero richiedere informazioni calcolate da attività precedenti, e attualmente il modo in cui viene fatto è aggiungere campi a una classe "stato pipeline".

Ho pensato (e spero?) che ci sia un modo migliore per condividere le informazioni tra i passaggi della pipeline piuttosto che avere un oggetto dati con un campo di zillion, alcuni dei quali hanno senso per alcuni passaggi di elaborazione e non per altri. Sarebbe un grosso problema rendere questa classe sicura per i thread (non so se sarebbe possibile), non c'è modo di ragionare sui suoi invarianti (ed è probabile che non ne abbia).

Stavo sfogliando il libro dei modelli di Gang of Four per trovare qualche ispirazione, ma non mi sembrava che ci fosse una soluzione lì dentro (Memento era un po 'nello stesso spirito, ma non del tutto). Ho anche guardato in linea, ma nel secondo in cui cerchi "pipeline" o "flusso di lavoro" vieni sommerso dalle informazioni sulle pipe Unix o da motori e framework proprietari del flusso di lavoro.

La mia domanda è: come affronti il problema della registrazione dello stato di esecuzione di una pipeline di elaborazione del software, in modo che le attività successive possano utilizzare le informazioni calcolate da quelle precedenti? Immagino che la principale differenza con i pipe Unix sia che non ti interessa solo l'output dell'attività immediatamente precedente.

Come richiesto, alcuni pseudocodici per illustrare il mio caso d'uso:

L'oggetto "contesto della pipeline" ha una serie di campi che i diversi passaggi della pipeline possono popolare / leggere:

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
}

Ciascuno dei passaggi della pipeline è anche un oggetto:

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;
    }
}

Allo stesso modo per un ipotetico FooStep , che potrebbe richiedere la barra calcolata da BarStep prima di esso, insieme ad altri dati. E poi abbiamo la vera chiamata API:

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);
    }
}
    
posta RuslanD 20.12.2012 - 22:18
fonte

4 risposte

4

Il motivo principale per utilizzare un progetto di pipeline è che si vogliono disaccoppiare gli stadi. O perché uno stadio può essere utilizzato in più pipeline (come gli strumenti della shell Unix) o perché si ottiene un vantaggio di scalabilità (ovvero, è possibile spostarsi facilmente da un'architettura a nodo singolo a un'architettura a più nodi).

In entrambi i casi, a ogni fase della pipeline deve essere fornito tutto ciò di cui ha bisogno per svolgere il proprio lavoro. Non c'è motivo per cui non sia possibile utilizzare un archivio esterno (ad es. Database), ma nella maggior parte dei casi è meglio passare i dati da uno stadio all'altro.

Tuttavia, ciò non significa che devi o devi passare un oggetto messaggio grande con ogni campo possibile (anche se vedi sotto). Invece, ogni fase della pipeline dovrebbe definire le interfacce per i suoi messaggi di input e output, che identificano solo i dati di cui ha bisogno lo stadio.

Hai quindi molta flessibilità nel modo in cui implementi i tuoi oggetti messaggio reali. Un approccio consiste nell'utilizzare un enorme oggetto dati che implementa tutte le interfacce necessarie. Un altro è quello di creare classi wrapper attorno a un semplice Map . Un'altra ancora è creare una classe wrapper attorno a un database.

    
risposta data 27.12.2012 - 15:27
fonte
1

Ci sono alcuni pensieri che mi vengono in mente, il primo dei quali è che non ho abbastanza informazioni.

  • Ogni passo produce dati usati oltre la pipeline o ci interessa solo i risultati dell'ultima fase?
  • Ci sono molti dubbi sui big data? vale a dire. problemi di memoria, problemi di velocità, ecc.

Probabilmente le risposte mi faranno riflettere più attentamente sul design, tuttavia sulla base di ciò che hai detto ci sono 2 approcci che probabilmente prenderei in considerazione per primi.

Struttura ogni fase come se fosse un oggetto. L'ennesimo stadio avrebbe fasi da 1 a n-1 come elenco di delegati. Ogni fase racchiude i dati e il trattamento dei dati; ridurre complessità complessive e campi all'interno di ogni oggetto. Puoi anche fare in modo che le fasi successive accedano ai dati secondo necessità da fasi molto precedenti attraversando i delegati. Hai ancora un accoppiamento piuttosto stretto su tutti gli oggetti perché sono i risultati degli stadi (ad esempio tutti gli attrs) che sono importanti, ma sono significativamente ridotti e ogni fase / oggetto è probabilmente più leggibile e comprensibile. Puoi renderlo thread-safe rendendo l'elenco dei delegati pigro e utilizzando una coda thread-safe per popolare l'elenco dei delegati in ciascun oggetto, se necessario.

In alternativa, probabilmente farei qualcosa di simile a quello che stai facendo. Un enorme oggetto di dati che attraversa funzioni che rappresentano ogni fase. Questo è spesso molto più veloce e leggero, ma più complesso e soggetto a errori, perché è solo una grande pila di attributi dei dati. Ovviamente non thread-safe.

Onestamente ho fatto il più tardi uno più spesso per ETL e alcuni altri problemi simili. Mi sono concentrato sulle prestazioni a causa della quantità di dati piuttosto che della manutenibilità. Inoltre, erano pezzi unici che non sarebbero stati usati di nuovo.

    
risposta data 27.12.2012 - 09:45
fonte
1

Sembra un pattern a catena in GoF.

Un buon punto di partenza sarebbe guardare cosa fa catena di comuni .

A popular technique for organizing the execution of complex processing flows is the "Chain of Responsibility" pattern, as described (among many other places) in the classic "Gang of Four" design patterns book. Although the fundamental API contracts required to implement this design patten are extremely simple, it is useful to have a base API that facilitates using the pattern, and (more importantly) encouraging composition of command implementations from multiple diverse sources.

Towards that end, the Chain API models a computation as a series of "commands" that can be combined into a "chain". The API for a command consists of a single method (execute()), which is passed a "context" parameter containing the dynamic state of the computation, and whose return value is a boolean that determines whether or not processing for the current chain has been completed (true), or whether processing should be delegated to the next command in the chain (false).

The "context" abstraction is designed to isolate command implementations from the environment in which they are run (such as a command that can be used in either a Servlet or Portlet, without being tied directly to the API contracts of either of these environments). For commands that need to allocate resources prior to delegation, and then release them upon return (even if a delegated-to command throws an exception), the "filter" extension to "command" provides a postprocess() method for this cleanup. Finally, commands can be stored and looked up in a "catalog" to allow deferral of the decision on which command (or chain) is actually executed.

To maximize the usefulness of the Chain of Responsibility pattern APIs, the fundamental interface contracts are defined in a manner with zero dependencies other than an appropriate JDK. Convenience base class implementations of these APIs are provided, as well as more specialized (but optional) implementations for the web environment (i.e. servlets and portlets).

Given that command implementations are designed to conform with these recommendations, it should be feasible to utilize the Chain of Responsibility APIs in the "front controller" of a web application framework (such as Struts), but also be able to use it in the business logic and persistence tiers to model complex computational requirements via composition. In addition, separation of a computation into discrete commands that operate on a general purpose context allows easier creation of commands that are unit testable, because the impact of executing a command can be directly measured by observing the corresponding state changes in the context that is supplied...

    
risposta data 27.12.2012 - 04:37
fonte
0

Una prima soluzione che posso immaginare è rendere espliciti i passaggi. Ognuno di essi diventa un oggetto in grado di elaborare un dato e trasmetterlo al successivo oggetto del processo. Ogni processo produce un nuovo prodotto (idealmente immutabile), in modo che non vi sia interazione tra i processi e quindi non vi è alcun rischio dovuto alla condivisione dei dati. Se alcuni processi richiedono più tempo di altri, è possibile inserire un buffer tra due processi. Se utilizzi correttamente uno scheduler per il multithreading, assegnerà più risorse per svuotare i buffer.

Una seconda soluzione potrebbe essere quella di pensare "messaggio" anziché pipeline, possibilmente con un framework dedicato. Hai quindi alcuni "attori" che ricevono messaggi da altri attori e inviano altri messaggi ad altri attori. Organizzi i tuoi attori in una pipeline e dai i tuoi dati primari a un primo attore che avvia la catena. Non c'è condivisione di dati poiché la condivisione viene sostituita dall'invio di messaggi. So che il modello degli attori di Scala può essere utilizzato in Java, poiché qui non c'è nulla di specifico per Scala, ma non l'ho mai usato in un programma Java.

Le soluzioni sono simili e puoi implementare la seconda con la prima. Fondamentalmente, i concetti principali consistono nel trattare dati immutabili per evitare i problemi tradizionali dovuti alla condivisione dei dati e creare entità esplicite e indipendenti che rappresentano i processi nella tua pipeline. Se soddisfi queste condizioni, puoi facilmente creare pipeline chiare e semplici e usarle in un programma parallelo.

    
risposta data 21.12.2012 - 17:18
fonte

Leggi altre domande sui tag