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


38

指定された開始日と終了日に基づいて、合計金額を月額に分割する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無視されているのかについて混乱する可能性があります。関数のドキュメントでこの動作を指定します。

さらに、私はそれが悪い先例を設定し、それ自体に関係してはならない(つまりSRPに違反する)APIに責任を追加すると感じます。追加のコンシューマーtotalが、numMonthsおよびからの計算など、より多くのユースケースをサポートする関数を必要としているとしmonthlyPaymentます。この関数は、時間とともにますます複雑になります。

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

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


79
「最近、このAPIの消費者は、終了日ではなく月数を[提供]したかった」-これは取るに足らない要求です。彼らは、1か月または2か月の終わりのコードで、月数を適切な終了日に変換できます。
グラハム

12
これは、Flag Argumentアンチパターンのように見えます。また、いくつかの関数に分割することをお勧めします
njzk2

2
注意点として、そこにある参照-パラメータの同じ種類と数を受け入れ、それらに基づいて非常に異なる結果を生成できる機能がDate-あなたは文字列を供給することができ、日を決定するために解析することができます。ただし、このようにパラメータを処理することも非常に細かくなり、信頼性の低い結果をもたらす可能性があります。参照してくださいDate再び。正しく行うことは不可能ではありません-Momentはそれをより良く処理しますが、それにもかかわらず使用するのは非常に迷惑です。
VLAZ

わずかな接線でmonthlyPaymentは、が与えられているがtotal整数倍ではない場合の処理​​方法を考えたいかもしれません。また、値が整数であることが保証されていない場合に発生する可能性のある浮動小数点丸めエラーに対処する方法(例:total = 0.3およびmonthlyPayment = 0.1)。
イルマリ・カロネン

@Graham私はそれに反応しませんでした...私は次の声明に反応しました「これに応じて、APIチームは機能を変更しました...」 - 胎児の位置にロールアップし、揺れ始めます -それはどこでも構いませんその1行または2行のコードは、異なる形式の新しいAPI呼び出しか、呼び出し側で行われます。このように動作するAPI呼び出しを変更しないでください!
Baldrickk

回答:


99

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

元のもの:

function getPaymentBreakdown(total, startDate, endDate) 

終了日ではなく月数を提供するもの:

function getPaymentBreakdownByNoOfMonths(total, startDate, noOfMonths) 

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

function getPaymentBreakdownByMonthlyPayment(total, startDate, monthlyPayment) 

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

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

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

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


24
実際、コードの「一般的な」実装は単純にgetPaymentBreakdown(または実際にはこれら3つのいずれか)であり、他の2つの関数は引数を変換して呼び出すだけです。これら3つのいずれかの完全なコピーであるプライベート関数を追加するのはなぜですか?
ジャコモアルゼッタ

@GiacomoAlzetta:それは可能です。しかし、OPs関数の「戻り」部分のみを含む共通関数を提供することにより、実装がより簡単になり、パブリック3関数がこの関数をパラメーターinnerNumMonthstotalおよびで呼び出すことができると確信していますstartDate。3パラメーター関数でも同様に機能するのに、5パラメーターで複雑すぎる関数を保持するのはなぜですか(3つはほとんどオプションです(設定する必要がある場合を除く))。
Doc Brown

3
「5引数関数を保持する」と言うつもりはありませんでした。あなたが何らかの共通のロジックを持っているとき、このロジックはプライベートである必要はないと言っています。この場合、3つの関数すべてをリファクタリングして、パラメーターを開始日から終了日まで単純に変換することができます。そのため、一般getPaymentBreakdown(total, startDate, endDate)的な実装としてパブリック関数を使用できます。
ジャコモアルゼッタ

@GiacomoAlzetta:わかりました、誤解でした、私はあなたがgetPaymentBreakdown質問の2番目の実装について話していると思いました。
Doc Brown

これらすべてを提供したい場合は、明示的に「getPaymentBreakdownByStartAndEnd」と呼ばれる元のメソッドの新しいバージョンを追加し、元のメソッドを非推奨にするまで進みます。
エリック

20

さらに、私はそれが悪い先例を設定し、それ自体に関係してはならない(つまりSRPに違反する)APIに責任を追加すると感じます。追加のコンシューマーtotalが、numMonthsおよびからの計算など、より多くのユースケースをサポートする関数を必要としているとしmonthlyPaymentます。この関数は、時間とともにますます複雑になります。

正解です。

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

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

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

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


3
@DocBrownうん。そのような場合(Ruby、Python、JS)では、静的メソッドまたはクラスメソッドを使用するだけです。しかし、それは実装の詳細であり、特に私の答えのポイント(「タイプを使用する」)に関連するとは思わない。
アレクサンダー

2
そして、このアイデアは悲しいことに、開始日、合計支払い、毎月の支払いという3番目の要件で限界に達します-そして、関数はお金のパラメーターからDateIntervalを計算します-そして、あなたは日付範囲に金額を入れるべきではありません...
ファルコ

3
@DocBrown「既存の関数から型のコンストラクターにのみ問題をシフトします」はい、タイムコードを配置する場所にタイムコードを配置するため、マネーコードを配置する必要があります。それは単純なSRPなので、問題を「のみ」シフトさせると言ったときに何が得られるのかわかりません。それがすべての機能が行うことです。彼らはコードを消滅させず、より適切な場所に移動します。あなたの問題は何ですか?「しかし、おめでとう、少なくとも5人のアップボッターが餌になった」これは、あなたが思っていた(希望)よりもはるかに嫌悪感に聞こえます。
アレクサンダー

@Falcoそれは私にとって新しい方法のように聞こえます(この支払い計算機クラスではなく、DateInterval):calculatePayPeriod(startData, totalPayment, monthlyPayment)
Alexander

7

時々流fluentな表現がこれに役立ちます:

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]

8
流interfaceなインターフェイス自体は、特定のタスクを簡単にも難しくもしません。これは、Builderパターンに似ています。
VLAZ

8
実装はむしろ、あなたのような誤解の呼び出しを防ぐために必要がある場合にかかわらず、複雑になるforTotalAmount(1234).breakIntoPayments().byPeriod(2).monthly().withPaymentsOf(12.34).byDateRange(saleStart, saleEnd);
Bergi

4
開発者が本当に自分の足で撮影したい場合は、@ Bergiの簡単な方法があります。それでも、あなたが入れた例では、よりはるかに読みやすいですforTotalAmountAndBreakIntoPaymentsByPeriodThenMonthlyWithPaymentsOfButByDateRange(1234, 2, 12.34, saleStart, saleEnd);
DanielCuadra

5
@DanielCuadra私がやろうとしていた点は、あなたの答えは、相互に排他的な3つのパラメーターを持つというOPの問題を実際には解決しないということです。ビルダーパターンを使用すると、呼び出しが読みやすくなります(そして、ユーザーが意味をなさないことに気付く可能性が高まります)が、ビルダーパターンを単独で使用しても、一度に3つの値を渡すことを防ぐことはできません。
ベルギ

2
@ファルコ はい、可能ですが、より複雑であり、答えはこれについて言及していません。私が見たより一般的なビルダーは、単一のクラスのみで構成されていました。ビルダーのコードを含めるように回答が編集された場合、喜んでそれを支持し、私のダウン投票を削除します。
ベルギ

2

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

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

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

6
以下のビルダーパターンのように、これにより呼び出しが読みやすくなります(そして、ユーザーが意味をなさないことに気付く可能性が高まります)が、パラメーターに名前を付けても、ユーザーが一度に3つの値を渡すことを防ぐことはできませんgetPaymentBreakdown(100, today, {endDate: whatever, noOfMonths: 4, monthlyPayment: 20})
Bergi

1
:代わりにすべきではありません=か?
バーマー

パラメーターの1つだけがnullでない(または辞書にない)ことを確認できると思います。
Mateen Ulhaq

1
@Bergi-構文自体は、ユーザーが無意味なパラメーターを渡すことを妨げませんが、いくつかの検証を行い、エラーをスローすることができます
slebetman

@Bergi私は決してJavascriptの専門家ではありませんが、ES6のDestructuring Assignmentはここで役立つと思いますが、これに関する知識は非常に軽いです。
グレゴリーカリー

1

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

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

何が起こったのtotalstartDateパラメータ?
ベルギ

これは素晴らしいAPIのように思えますが、これらの4つの関数がどのように実装されるか想像してみてください。(バリアント型と共通のインターフェイスを使用すると、これは非常にエレガントになりますが、何を念頭に置いたのかは不明です)。
ベルギ

@Bergiが私の投稿を編集しました
Vinz243

0

また、識別された共用体/代数データ型を使用してシステムを操作する場合は、たとえばとして渡すことができますTimePeriodSpecification

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

そして、実際に実装するのに失敗するような問題は発生しません。


これは間違いなく、これらのようなタイプが利用可能な言語でこれにアプローチする方法です。私は質問を言語にとらわれないようにしましたが、場合によってはこのようなアプローチが可能になるため、使用する言語を考慮する必要があります。
CalMlynarczyk
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.