web-dev-qa-db-ja.com

OOP複数の計算を構成する方法は?

ユーザーデータ入力に基づいて実行する一連(ほぼ86)の計算を必要とするプロジェクトに現在取り組んでいます。問題は、各計算に一連の要件があることです。

  • 各計算アルゴリズムの実装の変更を区別できるように、version変数を保持できる必要があります。このようにして、アルゴリズムを変更するたびに、特定の計算で使用されたバージョンがわかります。
  • アプリケーション内の他のモジュール(つまり、8つのエンティティー)から特定のデータをロードできるようにして、それぞれがその操作に必要な情報を選択できるようにする必要があります。
  • "runnable"であるかどうかを判断できるはずです。これにより、抽出されたデータ(前の要件から)がいくつかを満たすことを確認するfunction(?)アルゴリズムが正しく実行されることを保証する各計算のカスタム基準。
  • それぞれ異なるアルゴリズム実装が必要です。
  • 一連の実行メトリック(ログ)を生成し、それらを保存します。たとえば、データフェッチ時間、アルゴリズム実行時間、および実行する特定の計算ごとに読み込まれるデータの量として参照されるsampleSizeなどです。

現在、私がやったことは、次の構造を持つ抽象クラスCalculationを作成しました:

abstract class Calculation<T, F> {
  /**
 * Logging Variables.
   */
  private initialDataFetchTime: Date;
  private finalDataFetchTime: Date;
  private initialAlgorithmTime: Date;
  private finalAlgorithmTime: Date;

  // Final result holding variable.
  private finalResult: T;

  // The coverage status for this calculation.
  private coverage: boolean;

  // Data to use within the algorithm.
  private data: F;

  // The version of the Calculation.
  public abstract version: string;

  // The form data from the User to be used.
  public static formData: FormData;

  /**
 * This is the abstract function to be implemented with
 * the operation to be performed with the data. Always
 * called after `loadData()`.
   */
  public abstract async algorithm(): Promise<T>;

  /**
 * This function should implement the data fetching
 * for this particular calculation. This function is always
 * called before `calculation()`.
   */
  public abstract async fetchData(): Promise<F>;

  /**
 * This is the abstract function that checks
 * if enough information is met to perform the
 * calculation. This function is called always
 * after `loadData()`.
   */
  public abstract async coverageValidation(): Promise<boolean>;

  /**
 * This is the public member function that is called
 * to perform the data-fetching operations of the
 * calculation. This is the first function to call.
   */
  public async loadData(): Promise<void> {
    // Get the initial time.
    this.initialDataFetchTime = new Date();

    /**
     * Here we run the data-fetching implementation for
     * this particular calculation.
     */
    this.data = await this.fetchData();

    // Store the final time.
    this.finalDataFetchTime = new Date();
  }

  /**
 * This is the public member function that is called
 * to perform the calculation on this field. This is
 * the last function to be called.
   */
  public async calculation(): Promise<T> {
    // Get the initial time.
    this.initialAlgorithmTime = new Date();

    /**
     * Here we run the algorithmic implementation for
     * this particular calculation.
     */
    this.finalResult = await this.algorithm();

    // Store the final time.
    this.finalAlgorithmTime = new Date();

    // Return the result.
    return this.finalResult;
  }

  /**
 * This is the public member function that is called
 * to perform the coverage-checking of this calculation.
 * This function should be called after the `loadData()`
 * and before `calculation()`.
   */
  public async coverageCheck(): Promise<boolean> {
    // Execute the check function.
    this.coverage = await this.coverageValidation();

    // Return result.
    return this.coverage;
  }

  /**
 * Set FormData statically to be used across calculations.¡
   */
  public static setFormData(formData: FormData): FormData {
    // Store report.
    this.formData = formData;

    // Return report.
    return this.formData;
  }

  /**
 * Get the coverage of this calculation.
   */
  public getCoverage(): boolean {
    return this.coverage;
  }

  /**
 * Get the data for this calculation.
   */
  public getData(): F {
    return this.data;
  }

  /**
 * Get the result for this calculation.
   */
  public getResult(): T {
    return this.finalResult;
  }

  /**
   * Function to get the class name.
   */
  private getClassName(): string {
    return this.constructor.name;
  }

  /**
   * Function to get the version for this calculation.
   */
  private getVersion(): string { return this.version; }

  /**
   * Get all the Valuation Logs for this Calculation.
   */
  public async getValuationLogs(): Promise<CreateValuationLogDTO[]> {
    // The array of results.
    const valuationLogs: CreateValuationLogDTO[] = [];

    // Log the time the algorithm took to execute.
    valuationLogs.Push({
      report: Calculation.formData,
      calculation: this.getClassName(),
      metric: 'Algorithm Execution Time',
      version: this.getVersion(),
      value:
        this.initialAlgorithmTime.getTime() - this.finalAlgorithmTime.getTime(),
    });

    // Log the time to fetch information.
    valuationLogs.Push({
      report: Calculation.formData,
      calculation: this.getClassName(),
      metric: 'Data Fetch Load Time',
      version: this.getVersion(),
      value:
        this.initialDataFetchTime.getTime() - this.finalDataFetchTime.getTime(),
    });

    // Sample size is calculated and not an issue for this matter.

    // Return the metrics.
    return valuationLogs;
  }
}

次に、前のクラスを拡張する計算ごとに後続のクラスを作成しました。

export class GeneralArea extends Calculation<number, GeneralAreaData> {
  /**
   * Versioning information.
   * These variable hold the information about the progress done to this
   * calculation algorithm. The `version`  field is a SemVer field which
   * stores the version of the current algorithm implementation.
   *
   * IF YOU MAKE ANY MODIFICATION TO THIS CALCULATION, PLEASE UPDATE THE
   * VERSION ACCORDINGLY.
   */
  public version = '1.0.0';

  // Dependencies.
  constructor(private readonly dataSource: DataSource) {
    super();
  }

  // 1) Fetch Information
  public async fetchData(): Promise<GeneralAreaData> {
    // Query the DB.
    const dataPoints = this.dataSource.getInformation(/**  **/);

    // Return the data object.
    return {
      mortgages: dataPoints,
    };
  }

  // 2) Validate Coverage.
  public async coverageValidation(): Promise<boolean> {
    // Load data.
    const data: GeneralAreaData = this.getData();

    // Validate to be more than 5 results.
    if (data.mortgages.length < 5) {
      return false;
    }

    // Everything correct.
    return true;
  }

  // 3) Algorithm
  public async algorithm(): Promise<number> {
    // Load data.
    const data: GeneralAreaData = this.getData();

    // Perform operation.
    const result: number = await Math.min.apply(
      Math,
      data.mortgages.map(mortgage => mortgage.price),
    );

    // Return the result.
    return result;
  }
}

/**
 * Interface that holds the structure of the data
 * used for this implementation.
 */
export interface GeneralAreaData {
  // Mortages based on some criteria.
  mortages: SomeDataEntity;
}

アイデアは、3つの基本的な操作を実行できるようにすることです。

  1. 計算ごとにデータをロードします。
  2. すべての計算のカバレッジを検証します。
  3. 前のステップで一般的な「true」が返された場合は、計算を実行します。

ただし、FormData(ユーザーがアップロードする情報)が保存されるため、このパターンではいくつかの問題が発生しました静的に。つまり、いくつかの計算がすでに実行されており、別のユーザーがアップロードを実行した場合、 FormDataを設定できません。他のユーザーの計算が複雑になるためです。ただし、各関数コンストラクターにFormDataを渡すのは大変な作業のように見えます(これが正しい方法であると思われる場合は、コードを書く心配はありません;))

たぶん、この検疫はここにあるのでしょうか?現在、最終的な実行は次のようになります。


public performCalculation(formData: FormData): Promise<FormDataWithCalculations> {
  // Set general form data.
  Calculation.setFormData(formData); // <--- Error in subsequent requests :(

  // Instance Calculations.
  const generalAreaCalculation: GeneralAreaCalculation = new GeneralAreaCalculation(/** data service **/);
  // 85 more instantiations...

  // Load data for Calculations.
  try {
    await Promise.all([
      generalAreaCalculation.loadData(),
      // 85 more invocations...
    ]);
  } catch(dataLoadError) { /** error handling **/ }

  // Check for coverage.
  const coverages: boolean[] = await Promise.all([
    generalAreaCalculation.coverageCheck(),
    // 85 more coverage checks...
  ]);

  // Reduce coverage.
  const covered: boolean = coverages.reduce((previousValue, coverage) => coverage && previousValue, true);

  // Check coverage.
  if (!covered) { /** Throw exception **/ }

  // Perform calculations!
  const result: FormDataWithCalculations = new FormDataWithCalculations(formData);

  try {
    result.generalAreaValue = generalAreaCalculation.calculation();
    // 85 more of this.
  } catch (algorithmsError) { /** error handling ***/ }

  /*
   (( Here should go the log collecting and storing, for each of the 85 calculations ))
  */

  // Return processed information.
  return result;
}

再利用可能、保守可能、そしてさらに重要なことに、テスト可能になることを意味するのであれば、私はあまりにも多くのコードを書くことを恐れていません(そうです、各計算をテストして、通常のケースとEdgeケースで想定されていることを確実に実行します、だからクラスは私のアプローチだったので、それぞれがテストを添付していました)しかし、私は85の関数(すでに使用されているもの)を書くだけでなく、これらのそれぞれを呼び出すのではなく、この膨大な量のコードを書くことに完全に圧倒されます。

パターンはありますか?ガイダンス?助言?参照?教材?私はこのコードをこれ以上減らすことができないようですが、誰かがこの種の問題のより良いパターンを知っている場合、そしてそれが役立つ場合はTypeScript(NodeJSとNestJS API)のコードがどのようにすべてであるかを理解するために尋ねたかったです有線接続。

私のひどい英語を前もって感謝し、謝罪します!

2
HumbertoWoody

私が対処できる最も単純で安全な解決策は、FormDataimmutableクラスにすることです(フォームデータが共有され、コピーせずに異なる計算間で渡される場合、サイドはありません効果、マルチスレッドのコンテキストでも)。次に、1つのFormDataオブジェクトを計算に渡して、最も意味のある計算を行い、この「フリーズ」されたデータのセットを計算全体で再利用します。

これには、FormDataをアップロードして不変にするコードでの作業が含まれる場合があります。しかし、それが計算コードの「多くの作業」に終わる場合、私はあなたが何か間違っていることをしていると思います:新しいメンバー変数にデータを保持し、からの個々の計算にデータを使用する代わりに、単一の共有静的グローバル変数からは、大きな変更ではなく、わずかな変更のように見えます。

2
Doc Brown