Ich arbeite daran, bestimmte Aspekte eines vorhandenen Webdienstes neu zu faktorisieren. Die Implementierung der Service-APIs erfolgt über eine Art "Verarbeitungs-Pipeline", in der Aufgaben nacheinander ausgeführt werden. Es ist nicht überraschend, dass für spätere Tasks möglicherweise Informationen erforderlich sind, die von früheren Tasks berechnet wurden. Derzeit erfolgt dies durch Hinzufügen von Feldern zu einer "Pipeline-Status" -Klasse.
Ich habe gedacht (und gehofft?), Dass es einen besseren Weg gibt, Informationen zwischen Pipelineschritten auszutauschen, als ein Datenobjekt mit zig Feldern zu haben, von denen einige für einige Verarbeitungsschritte und andere nicht sinnvoll sind. Es wäre ein großer Schmerz, diese Klasse threadsicher zu machen (ich weiß nicht, ob es überhaupt möglich wäre), es gibt keine Möglichkeit, über ihre Invarianten zu argumentieren (und es ist wahrscheinlich, dass es keine gibt).
Ich habe das Buch mit den Gang of Four-Designmustern durchgeblättert, um Inspiration zu finden, aber ich hatte nicht das Gefühl, dass darin eine Lösung steckt (Memento war einigermaßen im selben Sinne, aber nicht ganz). Ich habe auch online gesucht, aber sobald Sie nach "Pipeline" oder "Workflow" suchen, werden Sie entweder mit Unix-Pipes-Informationen oder proprietären Workflow-Engines und Frameworks überflutet.
Meine Frage lautet: Wie würden Sie das Problem der Aufzeichnung des Ausführungsstatus einer Software-Verarbeitungs-Pipeline angehen, damit spätere Aufgaben die von früheren berechneten Informationen verwenden können? Ich denke, der Hauptunterschied zu Unix-Pipes ist, dass Sie sich nicht nur um die Ausgabe der unmittelbar vorhergehenden Aufgabe kümmern.
Wie gewünscht, ein Pseudocode zur Veranschaulichung meines Anwendungsfalls:
Das Objekt "Pipeline-Kontext" verfügt über eine Reihe von Feldern, die die verschiedenen Pipeline-Schritte füllen / lesen können:
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
}
Jeder der Pipelineschritte ist auch ein Objekt:
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;
}
}
Ähnliches gilt für eine FooStep
Hypothese, für die der von BarStep berechnete Balken zusammen mit anderen Daten erforderlich sein könnte. Und dann haben wir den wirklichen API-Aufruf:
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);
}
}
quelle
Antworten:
Der Hauptgrund für die Verwendung eines Pipeline-Designs besteht darin, dass Sie die Stufen entkoppeln möchten. Entweder, weil eine Stufe in mehreren Pipelines verwendet werden kann (wie die Unix-Shell-Tools), oder weil Sie einen gewissen Skalierungsvorteil erzielen (dh Sie können problemlos von einer Architektur mit einem Knoten zu einer Architektur mit mehreren Knoten wechseln).
In jedem Fall muss jeder Phase in der Pipeline alles gegeben werden, was sie für ihre Arbeit benötigt. Es gibt keinen Grund, warum Sie keinen externen Speicher (z. B. eine Datenbank) verwenden können, aber in den meisten Fällen ist es besser, die Daten von einer Stufe zur nächsten zu übertragen.
Dies bedeutet jedoch nicht, dass Sie mit jedem möglichen Feld ein großes Nachrichtenobjekt übergeben müssen oder sollten (siehe unten). Stattdessen sollte jede Phase in der Pipeline Schnittstellen für ihre Eingabe- und Ausgabenachrichten definieren, die nur die Daten identifizieren, die die Phase benötigt.
Sie haben dann viel Flexibilität bei der Implementierung Ihrer eigentlichen Nachrichtenobjekte. Ein Ansatz besteht darin, ein großes Datenobjekt zu verwenden, das alle erforderlichen Schnittstellen implementiert. Eine andere Möglichkeit besteht darin, Wrapper-Klassen um eine einfache herum zu erstellen
Map
. Eine weitere Möglichkeit besteht darin, eine Wrapper-Klasse für eine Datenbank zu erstellen.quelle
Es gibt ein paar Gedanken, die einem in den Sinn kommen, von denen einer ist, dass ich nicht genug Informationen habe.
Die Antworten ließen mich wahrscheinlich genauer über das Design nachdenken, aber auf der Grundlage dessen, was Sie sagten, gibt es zwei Ansätze, über die ich wahrscheinlich zuerst nachdenken würde.
Strukturieren Sie jede Stufe als eigenes Objekt. Die n-te Stufe hätte 1 bis n-1 Stufen als eine Liste von Delegierten. Die einzelnen Stufen kapseln die Daten und die Verarbeitung der Daten. Reduzierung der Gesamtkomplexität und der Felder in jedem Objekt. Sie können auch in späteren Phasen nach Bedarf auf die Daten zugreifen, indem Sie die Delegierten überqueren. Sie haben immer noch eine ziemlich enge Kopplung über alle Objekte, da die Ergebnisse der Phasen (dh alle Attribute) wichtig sind, diese jedoch erheblich reduziert sind und jede Phase / jedes Objekt wahrscheinlich besser lesbar und verständlicher ist. Sie können die Thread-Sicherheit erhöhen, indem Sie die Liste der Stellvertreter verzögern und die Stellvertreterliste in jedem Objekt nach Bedarf mit einer thread-sicheren Warteschlange füllen.
Alternativ würde ich wahrscheinlich etwas Ähnliches tun, wie Sie es tun. Ein massives Datenobjekt, das Funktionen durchläuft, die jede Phase darstellen. Dies ist oft viel schneller und leichter, aber komplexer und fehleranfälliger, da es sich nur um eine große Menge von Datenattributen handelt. Offensichtlich nicht threadsicher.
Ehrlich gesagt habe ich die spätere öfter für ETL und einige andere ähnliche Probleme gemacht. Ich habe mich eher auf die Leistung als auf die Wartbarkeit der Daten konzentriert. Sie waren auch Unikate, die nicht mehr verwendet werden würden.
quelle
Dies sieht aus wie ein Kettenmuster in GoF.
Ein guter Ausgangspunkt wäre, sich anzusehen, was die Commons-Chain macht.
quelle
Eine erste Lösung, die ich mir vorstellen kann, ist, die Schritte explizit zu machen. Jeder von ihnen wird zu einem Objekt, das Daten verarbeiten und an das nächste Prozessobjekt übertragen kann. Jeder Prozess erzeugt ein neues (im Idealfall unveränderliches) Produkt, sodass keine Wechselwirkung zwischen den Prozessen und kein Risiko durch Datenaustausch besteht. Wenn einige Prozesse zeitaufwendiger sind als andere, können Sie einen Puffer zwischen zwei Prozessen platzieren. Wenn Sie einen Scheduler für das Multithreading korrekt ausnutzen, werden mehr Ressourcen zum Leeren der Puffer zugewiesen.
Eine zweite Lösung könnte darin bestehen, "Nachricht" anstelle einer Pipeline zu denken, möglicherweise mit einem dedizierten Framework. Sie haben dann einige "Akteure", die Nachrichten von anderen Akteuren empfangen und andere Nachrichten an andere Akteure senden. Sie organisieren Ihre Akteure in einer Pipeline und geben Ihre Primärdaten an einen ersten Akteur weiter, der die Kette initiiert. Es findet keine gemeinsame Nutzung von Daten statt, da die gemeinsame Nutzung durch das Senden von Nachrichten ersetzt wird. Ich weiß, dass das Schauspielermodell von Scala in Java verwendet werden kann, da es hier nichts Spezifisches von Scala gibt, aber ich habe es nie in einem Java-Programm verwendet.
Die Lösungen sind ähnlich und Sie können die zweite mit der ersten implementieren. Grundsätzlich besteht das Hauptkonzept darin, sich mit unveränderlichen Daten zu befassen, um die herkömmlichen Probleme aufgrund des Datenaustauschs zu vermeiden und explizite und unabhängige Einheiten zu erstellen, die die Prozesse in Ihrer Pipeline darstellen. Wenn Sie diese Bedingungen erfüllen, können Sie problemlos klare, einfache Pipelines erstellen und in einem parallelen Programm verwenden.
quelle