web-dev-qa-db-ja.com

大規模でミッションクリティカルなメソッドを書き換えるためのアプローチ

コンテキスト:私は、大規模なレガシーメソッド(〜500行、プライベートメソッド呼び出しで拡張された〜2000行)のリファクタリングまたは再作成を担当する大規模なソフトウェア会社で大学を出たばかりの新しい採用者で、多くの責任を持つ複雑なワークフローを実行します。変換されたデータを返す前に、入力に複雑な一連の相互依存変換を適用することを想像できます。静的コード分析は、循環的複雑度が67であることを示しています。このクラスは、私のチームが所有していない複数のサービスで使用されるパッケージの一部であるため、インターフェイスを変更できません。このメソッドは、長期間にわたって機能ごとに非推奨になっているパッケージの一部です。リファクタリング/リライトの目的は、元の実装と機能の同等性を維持しながら、時間の経過とともに個々の変換を簡単に無効にできるようにすることです。メソッドにはテストがほとんどないため、最初のタスクは、クラスの安定した動作を実施するための完全なテストスイートを作成することです。

このリファクタリングを正常に実行するために、どのようなアプローチを適用できますか?私の現在のアイデアは、ある種のPipelineです。ここでは、パイプラインをアセンブルするときに無効にする変換を記述する列挙型のコレクションを取得します。この方法で将来的に変換を非推奨にするのは、パラメーターにenumを追加することです。例えば。の線に沿った何か

public class LegacyClass {
...
    public WorkflowResponse performWorkflow(WorkflowInput workflowInput) {
        WorkflowPipeline pipeline = this.assemblePipeline(this.featureDeprecations);
        pipeline.validate();
        return pipeline.execute();
    }

    private WorkflowPipeline assemblePipeline(EnumSet<FeatureDeprecations> featureDeprecations) {
        WorkflowPipeline pipeline = new WorkflowPipeline();
        pipeline.addTransformation(new TransformationOne());
        if(!featureDeprecations.contains(DEPRECATE_TRANSFORMATION_TWO)) {
            pipeline.addTransformation(new TransformationTwo());
        }
        pipeline.addTransformation(new TransformationThree());
        return pipeline;
    }

このアプローチに従うことは私の状況にとって良い考えですか?私が見ることができる主な欠点は、パイプラインをアセンブルするためのコードが、performWorkflowメソッド内で実行するのではなく、各変換をカプセル化しても、非常に複雑になることです。

1
hpabst

このアプローチに従うことは私の状況にとって良い考えですか?

この50.000フィートのビューのみが表示されている場合は、「はい」と答えます。複雑なプロセスを明確なデータフローを使用していくつかの小さな独立したステップに分割することは、このような種類のプロセスを管理しやすい状態に保つための優れた実践的なアプローチです。個々の変換ごとに単体テストを作成できます(そうする必要があります!)。それらを分離して実行することもできます。

私が見ることができる主な欠点は、パイプラインを組み立てるためのコードが非常に複雑になることです

ビジネスロジックと制御のフローをアセンブルコードに組み込むことから離れている場合はそうではありません。機能フラグの評価だけがロジックの種類である場合、組み立ては長くなるかもしれませんが、それほど複雑ではありません。

このようなリファクタリングの最も重要なルールを忘れないでください。十分な自動回帰テストが実施されていることを確認してくださいbefore何かを変更し始めます。次に、新しい変換を因数分解するたびに、これらのテストを頻繁に実行します。そうしないと、問題が発生します。このようなテストを作成するには、事前に多少の労力が必要になる場合がありますが、それだけの価値があります。

3
Doc Brown

それは確かに興味深い課題のようです。 @DocBrownとあなたが述べたように、コードを変更する前に必ずテストを行ってください。

私はパーティーに遅れるかもしれませんが、私はあなたがこれらのテストを非常に迅速に設定するためのテクニックを持っています。これは、「承認テスト」と呼ばれます(「ゴールデンマスター」や「特性評価テスト」のような他の名前があります)。

レシピは:

  1. 特定のコンテキストでメソッドを実行する
  2. 出力をキャプチャする
  3. メソッドがまだ予期した出力を生成することをテストします
  4. 別のテストを追加して、異なるコンテキストでメソッドを実行します
  5. 出力をキャプチャーし、メソッドをテストして、このコンテキストでこの出力を生成します
  6. すべてのシナリオをカバーするまで繰り返します(テストカバレッジが役立ちます)。

これは自動化できます https://approvaltests.com/ を見てください

そこに着くと、コードをいじって、何かを壊したかどうかを即座に知ることができます。

リファクタリングに関しては、あなたが説明したことが良いです。実際にデザインパターンを急いではいけません。コードを明確な責任に分割してください。

コードを操作することで、コードについてさらに理解し、関連する抽象化を見つけることができます。 ビジネスロジックをアセンブリコードから分離します。最終的には、明確な責任を単体テストするのが簡単になるはずです。

お役に立てば幸いです。このメソッドをリファクタリングしている間、遠慮なく質問してください。喜んでお手伝いします=)

1
nicoespeon

このクラスは、私のチームが所有していない複数のサービスで使用されるパッケージの一部であるため、インターフェースは変更できません

多くの場合、最大の勝利はこれらの依存関係を弱めることです。それはこの質問の範囲外であり、おそらく現時点では給与水準を上回っていますが、後で見る機会がある場合に備えて、心に留めておく価値があります。

変換されたデータを返す前に、複雑な一連の相互依存変換を入力に適用することを想像できます。

これはsoundパイプラインとまったく同じではありませんが、線形(またはとにかく非循環)データフローに正しくリファクタリングできれば、それ自体は明らかに改善されます。

リファクタリング/リライトの目的は、元の実装と機能の同等性を維持しながら、時間の経過とともに個々の変換を簡単に無効にできるようにすることです

十分に合理的な音。パイプラインステージを無効にすると、後続のステージへの入力に影響するため、適切なテストカバレッジを取得することが難しい場合があることに注意してください。原則として、実際に使用する予定のステージの組み合わせのみをテストできますが、後でリタイアシーケンスを並べ替える(または再優先順位付けする、または再検討する)能力が損なわれます。

メソッドにはテストがほとんどないため、最初のタスクは、クラスの安定した動作を実施するための完全なテストスイートを作成することです

常に良いアイデアです。変換を無効にすると結果も変わると想定すると、おそらくクライアントとの統合テストも必要になるでしょう。

あなたのスケッチされた実装に関して、私にはいくつかのことが起こります:

  1. 無効になっている機能を列挙すると、逆に感じます。

    1つのステージを廃止するのではなく、2つのステージをより良い実装にマージしたい場合はどうなりますか?パイプライン全体を構成可能にして、実行するステージを明示的に指定する(どのセットを省略するかではなく、暗黙的なセットから)と、より明確で柔軟になります。

    もちろん、それはあなたの特定のケースで意味をなすかもしれません。

  2. 毎回同じパイプラインを組み立てて実行するステートフルワークフローラッパーがあります。

    構成可能なパイプラインを作成している場合、おそらく独自のファクトリに値するでしょう。

    パイプラインをテスト可能にする場合は、内部的にステートレスにする必要もあります。その場合は、一度構築しておくだけでかまいません。

私が見ることができる主な欠点は、各変換をカプセル化しても、パイプラインを組み立てるためのコードが非常に複雑になることです。

比較的一般的なパイプラインファクトリを作成することは、実際には一部を無効にする複雑な関数を作成するよりも、実際には簡単であるように感じます。確かに、循環的複雑度は低くなります。個別の各ステージに名前を付けるだけで、文字列(または文字列の配列など)で全体を構成できます。

もちろん、これらはすべて、パイプラインが同種であることを前提としています。ステージに異なる入力タイプと出力タイプがある場合、ジェネリックアセンブリはより困難になります。

0
Useless