ソフトウェアパイプラインで共有データをカプセル化するための優れた実装戦略


13

既存のWebサービスの特定の側面のリファクタリングに取り組んでいます。サービスAPIの実装方法は、一連のタスクが順番に実行される「処理パイプライン」のようなものを持つことです。当然のことながら、後のタスクは前のタスクによって計算された情報を必要とする場合があり、現在これを行う方法は「パイプライン状態」クラスにフィールドを追加することです。

私は、無数のフィールドを持つデータオブジェクトを持つよりもパイプラインステップ間で情報を共有するより良い方法があると考えています(そして望んでいますか?)このクラスをスレッドセーフにするのは大きな苦痛になります(可能かどうかはわかりません)、その不変式について推論する方法はありません(そしておそらくないかもしれません)。

私はインスピレーションを見つけるためにGang of Fourのデザインパターンブックをページングしていましたが、そこに解決策があるとは感じませんでした(Mementoはやや同じ精神ですが、まったく同じではありませんでした)。私もオンラインで調べましたが、「パイプライン」または「ワークフロー」を検索する2番目のケースでは、Unixパイプ情報、または独自のワークフローエンジンとフレームワークのいずれかであふれています。

私の質問は、ソフトウェア処理パイプラインの実行状態を記録する問題にどのようにアプローチし、後のタスクが以前のタスクによって計算された情報を使用できるようにするかです。Unixパイプとの主な違いは、直前のタスクの出力だけを気にしないことです。


要求されたとおり、私のユースケースを説明するためのいくつかの擬似コード:

「パイプラインコンテキスト」オブジェクトには、さまざまなパイプラインステップで入力/読み取りできるフィールドがあります。

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
}

パイプラインの各ステップもオブジェクトです。

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

同様に、FooStep他のデータとともに、その前にBarStepによって計算されたBarを必要とするかもしれない仮想的なについても同様です。そして、実際の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
クロスポストしないで、MODが移動することを示すフラグ
ラチェットフリーク

1
これから先、ルールを理解するのにもっと時間を費やすべきだと思います。ありがとう!
ザラスランド

1
実装のために永続的なデータストレージを避けていますか、それともこの時点で何か手に入れるべきものはありますか?
CokoBWare

1
こんにちはRuslanDさん、ようこそ!これは実際、スタックオーバーフローよりもプログラマに適しているため、SOバージョンを削除しました。@ratchetfreakが言及したことを念頭に置いて、モデレーションに注意を向けてフラグを立て、より適切なサイトに移行する質問を尋ねることができます。クロスポストする必要はありません。2つのサイトを選択するための経験則は、プロジェクトを設計するホワイトボードの前にいるときに直面する問題をプログラマーが、より技術的な問題(実装の問題など)をスタックオーバーフローとすることです。詳細については、よくある質問をご覧ください
ヤニス

1
アーキテクチャをパイプラインではなく処理DAG(有向非巡回グラフ)に変更すると、以前の手順の結果を明示的に渡すことができます。
パトリック

回答:


4

パイプライン設計を使用する主な理由は、ステージを分離することです。1つのステージが複数のパイプライン(Unixシェルツールなど)で使用されるか、スケーリングのメリットが得られる(つまり、シングルノードアーキテクチャからマルチノードアーキテクチャに簡単に移行できる)ためです。

どちらの場合でも、パイプラインの各ステージには、その作業を行うために必要なすべてのものを与える必要があります。外部ストア(データベースなど)を使用できない理由はありませんが、ほとんどの場合、あるステージから別のステージにデータを渡す方が適切です。

ただし、それは、可能なフィールドごとに1つの大きなメッセージオブジェクトを渡す必要がある、または渡す必要があるという意味ではありません(以下を参照)。代わりに、パイプラインの各ステージは、ステージが必要とするデータのみを識別する、入力および出力メッセージのインターフェイスを定義する必要があります。

そうすれば、実際のメッセージオブジェクトの実装方法に大きな柔軟性があります。1つのアプローチは、必要なすべてのインターフェイスを実装する巨大なデータオブジェクトを使用することです。もう1つは、単純なMap。さらに別の方法は、データベースの周りにラッパークラスを作成することです。


1

頭に浮かぶいくつかの考えがあります。まず、十分な情報がないということです。

  • 各ステップはパイプラインを超えて使用されるデータを生成しますか、それとも最終段階の結果のみを考慮しますか?
  • ビッグデータに関する懸念事項は多くありますか?すなわち。メモリの問題、速度の問題など

答えはおそらくデザインについてより慎重に考えることでしょうが、あなたが言ったことに基づいて、おそらく最初に検討する2つのアプローチがあります。

各ステージを独自のオブジェクトとして構築します。n番目のステージには、デリゲートのリストとして1〜n-1のステージがあります。各ステージは、データとデータの処理をカプセル化します。各オブジェクト内の全体的な複雑さとフィールドを減らします。また、デリゲートをトラバースすることにより、必要に応じて後のステージでデータにアクセスすることもできます。重要なのはステージの結果(つまり、すべての属性)であるため、すべてのオブジェクト間でかなり緊密な結合が残っていますが、大幅に削減され、各ステージ/オブジェクトはおそらくより読みやすく、理解しやすくなります。デリゲートのリストを遅延させ、スレッドセーフキューを使用して必要に応じて各オブジェクトのデリゲートリストを作成することにより、スレッドセーフにすることができます。

あるいは、私はおそらくあなたがしていることに似た何かをするでしょう。各ステージを表す関数を通過する大量のデータオブジェクト。これは多くの場合、はるかに高速で軽量ですが、データ属性の大きな山であるため、より複雑でエラーが発生しやすくなります。明らかにスレッドセーフではありません。

正直なところ、ETLやその他の同様の問題については、後者をより頻繁に行いました。保守性よりもデータ量のせいでパフォーマンスに集中しました。また、それらは二度と使用されないものでした。


1

これは、GoFのチェーンパターンのように見えます。

良い出発点は何を見ることです commons-chainの機能です。

複雑な処理フローの実行を編成するための一般的な手法は、「責任の連鎖」パターンです。これは、古典的な「ギャングオブフォー」デザインパターンブックで(他の多くの場所の中でも)説明されています。この設計パターンを実装するために必要な基本的なAPIコントラクトは非常に単純ですが、パターンの使用を容易にし、(さらに重要なことに)複数の多様なソースからのコマンド実装の構成を促進するベースAPIがあると便利です。

そのために、Chain APIは一連の「コマンド」として計算をモデル化し、それらを「チェーン」に組み合わせることができます。コマンドのAPIは、単一のメソッド(execute())では、計算の動的状態を含む「コンテキスト」パラメーターが渡され、その戻り値は、現在のチェーンの処理が完了したかどうかを決定するブール値です( true)、または処理をチェーン内の次のコマンドに委任するかどうか(false)。

「コンテキスト」抽象化は、コマンド実装をそれらが実行される環境から分離するように設計されています(サーブレットまたはポートレットで使用できるコマンドなど、これらの環境のいずれかのAPIコントラクトに直接結び付けられることはありません)。委任の前にリソースを割り当て、リターン時にそれらを解放する必要があるコマンドの場合(委任先コマンドが例外をスローする場合でも)、「コマンド」に対する「フィルター」拡張機能がpostprocess()このクリーンアップの方法を提供します。最後に、コマンドを「カタログ」で保存および検索して、実際に実行するコマンド(またはチェーン)の決定を延期できます。

Chain of ResponsibilityパターンAPIの有用性を最大化するために、基本的なインターフェイスコントラクトは、適切なJDK以外のゼロの依存関係を持つ方法で定義されます。これらのAPIの便利な基本クラス実装と、Web環境(サーブレットおよびポートレットなど)向けのより特殊な(ただしオプションの)実装が提供されます。

コマンドの実装はこれらの推奨事項に適合するように設計されているため、Webアプリケーションフレームワーク(Strutsなど)の「フロントコントローラー」でチェーンオブレスポンシビリティAPIを利用することは可能ですが、ビジネスで使用することもできます。論理および永続化層を使用して、構成によって複雑な計算要件をモデル化します。さらに、汎用コンテキストで動作する個別のコマンドへの計算の分離により、ユニットテスト可能なコマンドを簡単に作成できます。これは、コマンドの実行の影響が、提供されるコンテキストの対応する状態変化を観察することで直接測定できるためです。 ...


0

私が想像できる最初の解決策は、ステップを明示的にすることです。これらはそれぞれ、データを処理して次のプロセスオブジェクトに送信できるオブジェクトになります。各プロセスは新しい(理想的には不変の)製品を生成するため、プロセス間の相互作用はなく、データ共有によるリスクはありません。一部のプロセスが他のプロセスよりも時間がかかる場合、2つのプロセスの間にバッファーを配置できます。マルチスレッド用のスケジューラを正しく活用すると、より多くのリソースを割り当ててバッファをフラッシュします。

2番目の解決策は、おそらく専用のフレームワークを使用して、パイプラインではなく「メッセージ」を考えることです。その後、他のアクターからメッセージを受信し、他のアクターに他のメッセージを送信する「アクター」がいくつかあります。アクターをパイプラインで整理し、チェーンを開始する最初のアクターにプライマリデータを渡します。共有はメッセージ送信に置き換えられるため、データ共有はありません。ここではScala固有のものは何もないので、ScalaのアクターモデルをJavaで使用できることは知っていますが、Javaプログラムで使用したことはありません。

ソリューションは類似しており、2番目のソリューションを最初のソリューションと実装できます。基本的に、主な概念は不変データを処理して、データ共有による従来の問題を回避し、パイプライン内のプロセスを表す明示的かつ独立したエンティティを作成することです。これらの条件を満たせば、明確で単純なパイプラインを簡単に作成し、それらを並列プログラムで使用できます。


ちょっと、私はいくつかの擬似コードで質問を更新しました-実際には明示的なステップがあります。
ザラスランド
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.