Bonnes stratégies de mise en œuvre pour encapsuler des données partagées dans un pipeline logiciel

13

Je travaille sur la refactorisation de certains aspects d'un service Web existant. La façon dont les API de service sont implémentées est d'avoir une sorte de "pipeline de traitement", où il y a des tâches qui sont exécutées en séquence. Sans surprise, les tâches ultérieures peuvent nécessiter des informations calculées par des tâches antérieures, et actuellement la manière de procéder consiste à ajouter des champs à une classe "état du pipeline".

J'ai pensé (et j'espère?) Qu'il y a une meilleure façon de partager des informations entre les étapes du pipeline que d'avoir un objet de données avec des champs de zillion, dont certains ont du sens pour certaines étapes de traitement et pas pour d'autres. Ce serait une douleur majeure de rendre cette classe thread-safe (je ne sais pas si ce serait même possible), il n'y a aucun moyen de raisonner sur ses invariants (et il est probable qu'elle n'en ait pas).

Je parcourais le livre sur les modèles de conception de Gang of Four pour trouver de l'inspiration, mais je n'avais pas l'impression qu'il y avait une solution (Memento était un peu dans le même esprit, mais pas tout à fait). J'ai également regardé en ligne, mais la seconde fois que vous recherchez "pipeline" ou "workflow", vous êtes inondé d'informations sur les tuyaux Unix ou de moteurs et frameworks de workflow propriétaires.

Ma question est la suivante: comment aborderiez-vous la question de l'enregistrement de l'état d'exécution d'un pipeline de traitement logiciel, afin que les tâches ultérieures puissent utiliser les informations calculées par les précédentes? Je suppose que la principale différence avec les canaux Unix est que vous ne vous souciez pas seulement de la sortie de la tâche immédiatement précédente.


Comme demandé, un pseudocode pour illustrer mon cas d'utilisation:

L'objet "contexte de pipeline" a un tas de champs que les différentes étapes du pipeline peuvent remplir / lire:

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
}

Chacune des étapes du pipeline est également un objet:

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

De même pour une hypothétique FooStep, qui pourrait avoir besoin de la barre calculée par BarStep avant elle, ainsi que d'autres données. Et puis nous avons le véritable appel 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);
    }
}
RuslanD
la source
6
ne pas traverser le poteau mais signaler un mod à déplacer
ratchet freak
1
Je vais continuer, je suppose que je devrais passer plus de temps à me familiariser avec les règles. Merci!
RuslanD
1
Évitez-vous le stockage de données persistantes pour votre implémentation, ou avez-vous quelque chose à gagner à ce stade?
CokoBWare
1
Salut RuslanD et bienvenue! C'est en effet plus adapté aux programmeurs que Stack Overflow, nous avons donc supprimé la version SO. Gardez à l'esprit ce que @ratchetfreak a mentionné, vous pouvez signaler l'attention de la modération et demander qu'une question soit migrée vers un site plus approprié, pas besoin de croiser les messages. La règle générale pour choisir entre les deux sites est que les programmeurs sont pour les problèmes que vous rencontrez lorsque vous êtes devant le tableau blanc lors de la conception de vos projets, et Stack Overflow est pour les problèmes plus techniques (par exemple, les problèmes d'implémentation). Pour plus de détails, consultez notre FAQ .
yannis
1
Si vous modifiez l'architecture en un DAG de traitement (graphique acyclique dirigé) au lieu d'un pipeline, vous pouvez explicitement transmettre les résultats des étapes précédentes.
Patrick

Réponses:

4

La principale raison d'utiliser une conception de pipeline est que vous souhaitez découpler les étapes. Soit parce qu'une étape peut être utilisée dans plusieurs pipelines (comme les outils shell Unix), soit parce que vous bénéficiez d'avantages de mise à l'échelle (c'est-à-dire que vous pouvez facilement passer d'une architecture à nœud unique à une architecture à nœuds multiples).

Dans les deux cas, chaque étape du pipeline doit recevoir tout ce dont elle a besoin pour faire son travail. Il n'y a aucune raison pour laquelle vous ne pouvez pas utiliser un magasin externe (par exemple, une base de données), mais dans la plupart des cas, il est préférable de passer les données d'une étape à l'autre.

Cependant, cela ne signifie pas que vous devez ou devez passer un gros objet de message avec tous les champs possibles (bien que voir ci-dessous). Au lieu de cela, chaque étape du pipeline doit définir des interfaces pour ses messages d'entrée et de sortie, qui identifient uniquement les données dont cette étape a besoin.

Vous disposez alors d'une grande flexibilité dans la manière d'implémenter vos objets de message réels. Une approche consiste à utiliser un énorme objet de données qui implémente toutes les interfaces nécessaires. Une autre consiste à créer des classes wrapper autour d'un simple Map. Une autre encore consiste à créer une classe wrapper autour d'une base de données.

parsifal
la source
1

Il y a quelques réflexions qui me viennent à l'esprit, la première étant que je n'ai pas assez d'informations.

  • Chaque étape produit-elle des données utilisées au-delà du pipeline, ou nous soucions-nous uniquement des résultats de la dernière étape?
  • Y a-t-il beaucoup de problèmes liés au Big Data? c'est à dire. problèmes de mémoire, problèmes de vitesse, etc.

Les réponses me feraient probablement réfléchir plus attentivement à la conception, mais sur la base de ce que vous avez dit, il y a 2 approches que je considérerais probablement en premier.

Structurez chaque étape comme son propre objet. La nième étape comporterait de 1 à n-1 étapes en tant que liste de délégués. Chaque étape encapsule les données et le traitement des données; réduire la complexité globale et les champs au sein de chaque objet. Vous pouvez également avoir des étapes ultérieures accéder aux données selon les besoins des étapes bien antérieures en parcourant les délégués. Vous avez toujours un couplage assez étroit entre tous les objets parce que ce sont les résultats des étapes (c'est-à-dire toutes les attrs) qui sont importants, mais ils sont considérablement réduits et chaque étape / objet est probablement plus lisible et compréhensible. Vous pouvez le rendre sûr pour les threads en rendant la liste des délégués paresseuse et en utilisant une file d'attente sécurisée pour les threads pour remplir la liste des délégués dans chaque objet selon vos besoins.

Sinon, je ferais probablement quelque chose de similaire à ce que vous faites. Un objet de données massif qui passe par des fonctions représentant chaque étape. C'est souvent beaucoup plus rapide et léger, mais plus complexe et sujet aux erreurs car il ne s'agit que d'un gros tas d'attributs de données. Évidemment pas thread-safe.

Honnêtement, j'ai fait le dernier plus souvent pour ETL et d'autres problèmes similaires. J'étais concentré sur les performances en raison de la quantité de données plutôt que de la maintenabilité. De plus, il s'agissait de pièces uniques qui ne seraient plus utilisées.

Dietbuddha
la source
1

Cela ressemble à un motif de chaîne dans GoF.

Un bon point de départ serait d'examiner ce que fait la chaîne des biens communs .

Une technique populaire pour organiser l'exécution de flux de traitement complexes est le modèle "Chaîne de responsabilité", comme décrit (parmi de nombreux autres endroits) dans le livre classique sur les modèles de conception "Gang of Four". Bien que les contrats d'API fondamentaux requis pour implémenter ce modèle de conception soient extrêmement simples, il est utile d'avoir une API de base qui facilite l'utilisation du modèle et (plus important encore) encourage la composition des implémentations de commandes à partir de multiples sources diverses.

À cette fin, l'API Chain modélise un calcul comme une série de "commandes" qui peuvent être combinées en une "chaîne". L'API d'une commande se compose d'une seule méthode ( execute()), à laquelle est passé un paramètre "context" contenant l'état dynamique du calcul, et dont la valeur de retour est un booléen qui détermine si le traitement de la chaîne en cours est terminé ou non ( true), ou si le traitement doit être délégué à la commande suivante de la chaîne (false).

L'abstraction "contextuelle" est conçue pour isoler les implémentations de commandes de l'environnement dans lequel elles sont exécutées (comme une commande qui peut être utilisée dans un servlet ou un portlet, sans être liée directement aux contrats d'API de l'un ou l'autre de ces environnements). Pour les commandes qui doivent allouer des ressources avant la délégation, puis les libérer au retour (même si une commande déléguée lève une exception), l'extension "filter" à "command" fournit une postprocess()méthode pour ce nettoyage. Enfin, les commandes peuvent être stockées et recherchées dans un "catalogue" pour permettre le report de la décision sur la commande (ou chaîne) qui est réellement exécutée.

Pour maximiser l'utilité des API de modèle de chaîne de responsabilité, les contrats d'interface fondamentaux sont définis d'une manière avec zéro dépendances autres qu'un JDK approprié. Des implémentations de classe de base pratiques de ces API sont fournies, ainsi que des implémentations plus spécialisées (mais facultatives) pour l'environnement Web (c.-à-d. Servlets et portlets).

Étant donné que les implémentations de commandes sont conçues pour se conformer à ces recommandations, il devrait être possible d'utiliser les API de la chaîne de responsabilité dans le «contrôleur frontal» d'un cadre d'application Web (comme Struts), mais également de pouvoir l'utiliser dans l'entreprise. niveaux de logique et de persistance pour modéliser des exigences de calcul complexes via la composition. De plus, la séparation d'un calcul en commandes discrètes qui opèrent dans un contexte général permet une création plus facile de commandes testables à l'unité, car l'impact de l'exécution d'une commande peut être directement mesuré en observant les changements d'état correspondants dans le contexte fourni. ...

Aldrin Leal
la source
0

Une première solution que je peux imaginer est de rendre les étapes explicites. Chacun d'eux devient un objet capable de traiter une donnée et de la transmettre à l'objet de processus suivant. Chaque processus produit un nouveau produit (idéalement immuable), de sorte qu'il n'y a pas d'interaction entre les processus et ensuite il n'y a aucun risque dû au partage de données. Si certains processus prennent plus de temps que d'autres, vous pouvez placer un tampon entre deux processus. Si vous exploitez correctement un planificateur pour le multithreading, il allouera plus de ressources pour vider les tampons.

Une deuxième solution pourrait être de penser «message» au lieu de pipeline, éventuellement avec un framework dédié. Vous avez alors des "acteurs" qui reçoivent des messages d'autres acteurs et envoient d'autres messages à d'autres acteurs. Vous organisez vos acteurs dans un pipeline et donnez vos données primaires à un premier acteur qui initie la chaîne. Il n'y a pas de partage de données puisque le partage est remplacé par l'envoi de messages. Je sais que le modèle d'acteur de Scala peut être utilisé en Java, car il n'y a rien de spécifique à Scala ici, mais je ne l'ai jamais utilisé dans un programme Java.

Les solutions sont similaires et vous pouvez implémenter la seconde avec la première. Fondamentalement, les concepts principaux sont de traiter des données immuables pour éviter les problèmes traditionnels dus au partage de données et de créer des entités explicites et indépendantes représentant les processus dans votre pipeline. Si vous remplissez ces conditions, vous pouvez facilement créer des pipelines clairs et simples et les utiliser dans un programme parallèle.

mgoeminne
la source
Hé, j'ai mis à jour ma question avec un pseudocode - nous avons en fait les étapes explicites.
RuslanD