Buone strategie di implementazione per incapsulare i dati condivisi in una pipeline di software


13

Sto lavorando al re-factoring di alcuni aspetti di un servizio web esistente. Il modo in cui vengono implementate le API di servizio è avere una sorta di "pipeline di elaborazione", in cui vi sono attività che vengono eseguite in sequenza. Non sorprende che le attività successive possano richiedere informazioni calcolate dalle attività precedenti e attualmente il modo in cui ciò viene fatto è aggiungendo campi a una classe "stato della pipeline".

Ho pensato (e spero?) Che esista un modo migliore per condividere le informazioni tra i passaggi della pipeline piuttosto che avere un oggetto dati con campi da un milione di miliardi, alcuni dei quali hanno senso per alcuni passaggi di elaborazione e non per altri. Sarebbe una grande sofferenza rendere questa classe thread-safe (non so se sarebbe nemmeno possibile), non c'è modo di ragionare sui suoi invarianti (ed è probabile che non ne abbia).

Stavo sfogliando il libro dei modelli di design di Gang of Four per trovare un po 'di ispirazione, ma non mi sembrava che ci fosse una soluzione (Memento era un po' nello stesso spirito, ma non del tutto). Ho anche cercato online, ma nel momento in cui cerchi "pipeline" o "flusso di lavoro", sei invaso dalle informazioni sui tubi Unix o dai motori e dai framework di flusso di lavoro proprietari.

La mia domanda è: come affronteresti il ​​problema della registrazione dello stato di esecuzione di una pipeline di elaborazione software, in modo che le attività successive possano utilizzare le informazioni calcolate da quelle precedenti? Immagino che la differenza principale con le 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 vari 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 aver bisogno della barra calcolata da BarStep prima di essa, 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);
    }
}

6
non attraversare la posta ma contrassegna la mossa di una mod
maniaco del cricchetto

1
Andrà avanti, immagino che dovrei passare più tempo a familiarizzare con le regole. Grazie!
RuslanD,

1
Stai evitando l'archiviazione persistente dei dati per la tua implementazione o, a questo punto, c'è qualcosa di utile?
CokoBWare,

1
Ciao RuslanD e benvenuto! Questo è davvero più adatto ai programmatori rispetto allo StackTranslate.it, quindi abbiamo rimosso la versione SO. Tieni presente ciò che @ratchetfreak ha menzionato, puoi segnalare l'attenzione per la moderazione e chiedere che una domanda venga migrata su un sito più adatto, senza bisogno di cross post. La regola empirica per la scelta tra i due siti è che i programmatori sono per i problemi che si incontrano quando ci si trova davanti alla lavagna che progetta i progetti e Stack Overflow è per problemi più tecnici (ad esempio problemi di implementazione). Per maggiori dettagli vedi le nostre FAQ .
yannis,

1
Se si modifica l'architettura in un DAG di elaborazione (grafico aciclico diretto) anziché in una pipeline, è possibile passare esplicitamente i risultati dei passaggi precedenti.
Patrick,

Risposte:


4

Il motivo principale per utilizzare un progetto di pipeline è che si desidera disaccoppiare le fasi. O perché uno stadio può essere utilizzato in più pipeline (come gli strumenti della shell Unix) o perché si ottengono alcuni vantaggi di ridimensionamento (ovvero, è possibile passare facilmente da un'architettura a nodo singolo a un'architettura a più nodi).

In entrambi i casi, ogni fase della pipeline deve ricevere tutto ciò di cui ha bisogno per svolgere il proprio lavoro. Non vi è alcun motivo per cui non è possibile utilizzare un archivio esterno (ad esempio, un database), ma nella maggior parte dei casi è meglio trasferire i dati da uno stadio all'altro.

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

Quindi hai 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. Ancora un altro è quello di creare una classe wrapper attorno a un database.


1

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

  • Ogni passaggio produce dati utilizzati oltre la pipeline o ci preoccupiamo solo dei risultati dell'ultima fase?
  • Ci sono molte preoccupazioni relative ai big data? vale a dire. problemi di memoria, problemi di velocità, ecc

Le risposte probabilmente mi farebbero riflettere più attentamente sul design, tuttavia in base a ciò che hai detto ci sono 2 approcci che probabilmente prenderei in considerazione per primo.

Strutturare ogni fase come il proprio oggetto. L'ennesima tappa avrebbe una fase da 1 a n-1 come un elenco di delegati. Ciascuna fase incapsula i dati e l'elaborazione dei dati; riducendo la complessità generale e i campi all'interno di ciascun oggetto. È inoltre possibile che le fasi successive accedano ai dati secondo necessità da fasi molto precedenti attraversando i delegati. Hai ancora un accoppiamento piuttosto stretto tra tutti gli oggetti perché sono i risultati degli stadi (cioè tutti gli attr) che sono importanti, ma è significativamente ridotto e ogni stadio / oggetto è probabilmente più leggibile e comprensibile. È possibile rendere sicuro il thread rendendo pigro l'elenco dei delegati 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 dati che passa attraverso funzioni che rappresentano ogni fase. Questo è spesso molto più veloce e leggero, ma più complesso e soggetto a errori a causa del fatto che è solo un grande mucchio di attributi di dati. Ovviamente non thread-safe.

Sinceramente ho fatto quello successivo più spesso per ETL e altri problemi simili. Mi sono concentrato sulle prestazioni a causa della quantità di dati piuttosto che della manutenibilità. Inoltre, erano una tantum che non sarebbero stati riutilizzati.


1

Sembra un modello a catena in GoF.

Un buon punto di partenza sarebbe quello di fare ciò che fa la catena comune .

Una tecnica popolare per organizzare l'esecuzione di flussi di elaborazione complessi è il modello "Catena di responsabilità", come descritto (tra molti altri luoghi) nel classico libro di modelli di design "Banda di quattro". Sebbene i contratti API fondamentali richiesti per implementare questo patten di progettazione siano estremamente semplici, è utile disporre di un'API di base che faciliti l'utilizzo del modello e (cosa più importante) che incoraggi la composizione delle implementazioni dei comandi da più fonti diverse.

A tal fine, l'API Chain modella un calcolo come una serie di "comandi" che possono essere combinati in una "catena". L'API per un comando è costituita da un singolo metodo ( execute()), al quale viene passato un parametro "contesto" contenente lo stato dinamico del calcolo e il cui valore di ritorno è un valore booleano che determina se l'elaborazione per la catena corrente è stata completata ( true) o se l'elaborazione deve essere delegata al comando successivo nella catena (false).

L'astrazione "contesto" è progettata per isolare le implementazioni dei comandi dall'ambiente in cui vengono eseguite (come un comando che può essere utilizzato in un servlet o in un portlet, senza essere legato direttamente ai contratti API di uno di questi ambienti). Per i comandi che devono allocare risorse prima della delega e quindi rilasciarle al momento della restituzione (anche se un comando delegato a genera un'eccezione), l'estensione "filtro" a "comando" fornisce un postprocess()metodo per questa pulizia. Infine, i comandi possono essere memorizzati e cercati in un "catalogo" per consentire il differimento della decisione su quale comando (o catena) viene effettivamente eseguito.

Per massimizzare l'utilità delle API del modello Chain of Responsibility, i contratti di interfaccia fondamentali sono definiti in modo con zero dipendenze diverse da un JDK appropriato. Vengono fornite implementazioni della classe base di convenienza di queste API, nonché implementazioni più specializzate (ma facoltative) per l'ambiente Web (ovvero servlet e portlet).

Dato che le implementazioni dei comandi sono progettate per conformarsi a queste raccomandazioni, dovrebbe essere possibile utilizzare le API Chain of Responsibility nel "front controller" di un framework di applicazioni Web (come Struts), ma anche essere in grado di utilizzarlo nel business livelli logici e di persistenza per modellare complessi requisiti computazionali tramite composizione. Inoltre, la separazione di un calcolo in comandi discreti che operano in un contesto di uso generale consente una più facile creazione di comandi che sono testabili in unità, poiché l'impatto dell'esecuzione di un comando può essere misurato direttamente osservando i corrispondenti cambiamenti di stato nel contesto che viene fornito ...


0

Una prima soluzione che posso immaginare è quella di rendere espliciti i passaggi. Ognuno di essi diventa un oggetto in grado di elaborare un dato e trasmetterlo al successivo oggetto di processo. Ogni processo produce un nuovo prodotto (idealmente immutabile), in modo che non vi sia interazione tra i processi e quindi non vi siano rischi dovuti alla condivisione dei dati. Se alcuni processi richiedono più tempo di altri, è possibile posizionare un buffer tra due processi. Se si sfrutta correttamente uno scheduler per il multithreading, verranno allocate più risorse per svuotare i buffer.

Una seconda soluzione potrebbe essere quella di pensare al "messaggio" anziché alla pipeline, possibilmente con un framework dedicato. Quindi alcuni "attori" 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 esiste condivisione dei dati poiché la condivisione è sostituita dall'invio di messaggi. So che il modello di attore di Scala può essere usato in Java, dal momento che 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 sono quelli di trattare dati immutabili per evitare i problemi tradizionali dovuti alla condivisione dei dati e creare entità esplicite e indipendenti che rappresentino i processi nella tua pipeline. Se si soddisfano queste condizioni, è possibile creare facilmente condutture chiare e semplici e utilizzarle in un programma parallelo.


Ehi, ho aggiornato la mia domanda con alcuni pseudocodici - in effetti abbiamo i passaggi espliciti.
RuslanD,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.