web-dev-qa-db-ja.com

競合する関数パラメーターを処理するパターンはありますか?

所定の開始日と終了日に基づいて、合計金額を月額に分解するAPI関数があります。

// JavaScript

function convertToMonths(timePeriod) {
  // ... returns the given time period converted to months
}

function getPaymentBreakdown(total, startDate, endDate) {
  const numMonths = convertToMonths(endDate - startDate);

  return {
    numMonths,
    monthlyPayment: total / numMonths,
  };
}

最近、このAPIの利用者は、1)終了日の代わりに月数を提供することにより、または2)毎月の支払いを提供し、終了日を計算することにより、日付範囲を他の方法で指定することを望んでいました。これに対応して、APIチームは機能を次のように変更しました。

// JavaScript

function addMonths(date, numMonths) {
  // ... returns a new date numMonths after date
}

function getPaymentBreakdown(
  total,
  startDate,
  endDate /* optional */,
  numMonths /* optional */,
  monthlyPayment /* optional */,
) {
  let innerNumMonths;

  if (monthlyPayment) {
    innerNumMonths = total / monthlyPayment;
  } else if (numMonths) {
    innerNumMonths = numMonths;
  } else {
    innerNumMonths = convertToMonths(endDate - startDate);
  }

  return {
    numMonths: innerNumMonths,
    monthlyPayment: total / innerNumMonths,
    endDate: addMonths(startDate, innerNumMonths),
  };
}

この変更によりAPIが複雑になると思います。ここで、呼び出し元は、日付範囲の計算に使用されるパラメーターを優先するかどうかを決定する際に、関数の実装に隠されているヒューリスティックを心配する必要があります(つまり、優先度順monthlyPaymentnumMonthsendDate)。呼び出し元が関数のシグネチャに注意を払わないと、複数のオプションパラメータが送信され、endDateが無視されている理由が混乱する可能性があります。この動作は関数ドキュメントで指定します。

さらに、私はそれが悪い前例を設定し、APIに自分自身が関与してはならない責任(つまり、SRP違反)を追加していると感じています。追加のコンシューマーが、totalおよびnumMonthsパラメーターからmonthlyPaymentを計算するなど、関数がより多くのユースケースをサポートすることを望んでいるとします。この機能は、時間の経過とともにますます複雑になります。

私の好みは、関数をそのまま維持し、代わりに呼び出し元がendDateを自分で計算することを要求することです。しかし、私は間違っている可能性があり、彼らが行った変更がAPI関数を設計するための許容できる方法であるかどうか疑問に思っていました。

または、このようなシナリオを処理するための一般的なパターンはありますか?元の関数をラップする追加の高次関数をAPIで提供することもできますが、これはAPIを膨らませます。関数内で使用する方法を指定するフラグパラメータを追加することもできます。

38
CalMlynarczyk

実装を見ると、ここで本当に必要なものは1つではなく3つの異なる関数であるように見えます。

オリジナルのもの:

function getPaymentBreakdown(total, startDate, endDate) 

終了日の代わりに月数を提供するもの:

function getPaymentBreakdownByNoOfMonths(total, startDate, noOfMonths) 

そして、毎月の支払いを提供し、終了日を計算するもの:

function getPaymentBreakdownByMonthlyPayment(total, startDate, monthlyPayment) 

これで、オプションのパラメーターはなくなり、どの関数がどのように、どの目的で呼び出されるかが明確になります。コメントで述べたように、厳密に型指定された言語では、関数のオーバーロードを利用して、3つの異なる関数を必ずしも名前ではなく、シグネチャで区別することもできます。

異なる関数は、ロジックを複製する必要があることを意味しないことに注意してください。内部的に、これらの関数が共通のアルゴリズムを共有する場合は、「プライベート」関数にリファクタリングする必要があります。

このようなシナリオを処理するための一般的なパターンはありますか

優れたAPI設計を説明する(GoF設計パターンの意味での)「パターン」はないと思います。自己記述的な名前、パラメーターの少ない関数、直交(=独立)パラメーターのある関数を使用することは、可読性、保守性、および発展性のあるコードを作成するための基本原則にすぎません。プログラミングのすべての良いアイデアが必ずしも「設計パターン」であるとは限りません。

99
Doc Brown

さらに、私はそれが悪い前例を設定し、APIに自分自身が関与してはならない責任(つまり、SRP違反)を追加していると感じています。追加のコンシューマーが、totalおよびnumMonthsパラメーターからmonthlyPaymentを計算するなど、関数がより多くのユースケースをサポートすることを望んでいるとします。この機能は、時間の経過とともにますます複雑になります。

あなたは正確です。

私の好みは、関数をそのまま維持し、代わりに呼び出し側がendDateを自分で計算することを要求することです。しかし、私は間違っている可能性があり、彼らが行った変更がAPI関数を設計するための許容できる方法であるかどうか疑問に思っていました。

呼び出し元のコードが無関係なボイラープレートで汚染されるため、これも理想的ではありません。

または、このようなシナリオを処理するための一般的なパターンはありますか?

DateIntervalのような新しいタイプを導入します。意味のあるコンストラクタを追加します(開始日+終了日、開始日+ numか月など)。システム全体で日付/時刻の間隔を表すための共通通貨タイプとしてこれを採用してください。

時々、流れるような表現がこれに役立ちます:

let payment1 = forTotalAmount(1234)
                  .breakIntoPayments()
                  .byPeriod(months(2));

let payment2 = forTotalAmount(1234)
                  .breakIntoPayments()
                  .byDateRange(saleStart, saleEnd);

let monthsDue = forTotalAmount(1234)
                  .calculatePeriod()
                  .withPaymentsOf(12.34)
                  .monthly();

設計するための十分な時間が与えられれば、ドメイン固有の言語と同様に機能する堅固なAPIを思い付くことができます。

もう1つの大きな利点は、オートコンプリートを使用するIDEが、自己発見可能な機能により直感的に理解できるように、APIドキュメントを読むのにほとんど意味がないほとんどないことです。

https://nikas.praninskas.com/javascript/2015/04/26/fluent-javascript/ または https://github.com/nikaspranなどのリソースがあります。 /fluent.js このトピックについて。

例(最初のリソースリンクから取得):

let insert = (value) => ({into: (array) => ({after: (afterValue) => {
  array.splice(array.indexOf(afterValue) + 1, 0, value);
  return array;
}})});

insert(2).into([1, 3]).after(1); //[1, 2, 3]
7
DanielCuadra

まあ、他の言語では、名前付きパラメーターを使用します。これはJavaScriptでエミュレートできます:

function getPaymentBreakdown(total, startDate, durationSpec) { ... }

getPaymentBreakdown(100, today, {endDate: whatever});
getPaymentBreakdown(100, today, {noOfMonths: 4});
getPaymentBreakdown(100, today, {monthlyPayment: 20});
2
Gregory Currie

別の方法として、月数を指定する責任をなくし、関数から除外することもできます。

getPaymentBreakdown(420, numberOfMonths(3))
getPaymentBreakdown(420, dateRage(a, b))
getPaymentBreakdown(420, paymentAmount(350))

そしてgetpaymentBreakdownは、基本の月数を提供するオブジェクトを受け取ります

それらは、例えば関数を返す高次関数になります。

function numberOfMonths(months) {
  return {months: (total) => months};
}

function dateRange(startDate, endDate) {
  return {months: (total) => convertToMonths(endDate - startDate)}
}

function monthlyPayment(amount) {
  return {months: (total) => total / amount}
}


function getPaymentBreakdown(total, {months}) {
  const numMonths= months(total);
  return {
    numMonths, 
    monthlyPayment: total / numMonths,
    endDate: addMonths(startDate, numMonths)
  };
}
1
Vinz243

そして、あなたが区別された共用体/代数的データ型を持つシステムで作業しているなら、あなたはそれを例えばTimePeriodSpecificationとして渡すことができます。

type TimePeriodSpecification =
    | DateRange of startDate : DateTime * endDate : DateTime
    | MonthCount of startDate : DateTime * monthCount : int
    | MonthlyPayment of startDate : DateTime * monthlyAmount : float

そして、実際に1つを実装できないなどの問題は発生しません。

0
NiklasJ