it-swarm.com.de

Gibt es ein Muster für den Umgang mit widersprüchlichen Funktionsparametern?

Wir haben eine API-Funktion, die einen Gesamtbetrag basierend auf den angegebenen Start- und Enddaten in monatliche Beträge aufteilt.

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

Kürzlich wollte ein Verbraucher für diese API den Datumsbereich auf andere Weise angeben: 1) durch Angabe der Anzahl der Monate anstelle des Enddatums oder 2) durch Angabe der monatlichen Zahlung und Berechnung des Enddatums. Als Reaktion darauf änderte das API-Team die Funktion wie folgt:

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

Ich bin der Meinung, dass diese Änderung die API kompliziert. Jetzt muss sich der Aufrufer um die Heuristiken kümmern, die bei der Implementierung der Funktion verborgen sind, um zu bestimmen, welche Parameter bei der Berechnung des Datumsbereichs bevorzugt werden (dh in der Reihenfolge der Priorität monthlyPayment, numMonths, endDate). Wenn ein Aufrufer die Funktionssignatur nicht beachtet, sendet er möglicherweise mehrere der optionalen Parameter und ist verwirrt darüber, warum endDate ignoriert wird. Dieses Verhalten geben wir in der Funktionsdokumentation an.

Außerdem habe ich das Gefühl, dass es einen schlechten Präzedenzfall darstellt und der API Verantwortlichkeiten hinzufügt, mit denen es sich nicht befassen sollte (d. H. SRP verletzt). Angenommen, zusätzliche Benutzer möchten, dass die Funktion mehr Anwendungsfälle unterstützt, z. B. die Berechnung von total aus den Parametern numMonths und monthlyPayment. Diese Funktion wird mit der Zeit immer komplizierter.

Ich bevorzuge es, die Funktion so zu belassen, wie sie war, und stattdessen den Aufrufer zu verpflichten, endDate selbst zu berechnen. Ich kann mich jedoch irren und habe mich gefragt, ob die vorgenommenen Änderungen eine akzeptable Möglichkeit zum Entwerfen einer API-Funktion darstellen.

Gibt es alternativ ein allgemeines Muster für den Umgang mit solchen Szenarien? Wir könnten zusätzliche Funktionen höherer Ordnung in unserer API bereitstellen, die die ursprüngliche Funktion umschließen, aber dies bläht die API auf. Vielleicht könnten wir einen zusätzlichen Flag-Parameter hinzufügen, der angibt, welcher Ansatz innerhalb der Funktion verwendet werden soll.

38
CalMlynarczyk

Wenn ich die Implementierung sehe, scheint es mir, dass Sie hier wirklich 3 verschiedene Funktionen anstelle von einer benötigen:

Das Original:

function getPaymentBreakdown(total, startDate, endDate) 

Derjenige, der die Anzahl der Monate anstelle des Enddatums angibt:

function getPaymentBreakdownByNoOfMonths(total, startDate, noOfMonths) 

und derjenige, der die monatliche Zahlung bereitstellt und das Enddatum berechnet:

function getPaymentBreakdownByMonthlyPayment(total, startDate, monthlyPayment) 

Jetzt gibt es keine optionalen Parameter mehr und es sollte ziemlich klar sein, welche Funktion wie und zu welchem ​​Zweck aufgerufen wird. Wie in den Kommentaren erwähnt, könnte man in einer streng typisierten Sprache auch eine Funktionsüberladung verwenden, wobei die drei verschiedenen Funktionen nicht unbedingt durch ihren Namen, sondern durch ihre Signatur unterschieden werden, falls dies ihren Zweck nicht verschleiert.

Beachten Sie, dass die verschiedenen Funktionen nicht bedeuten, dass Sie eine Logik duplizieren müssen. Wenn diese Funktionen einen gemeinsamen Algorithmus verwenden, sollte sie intern in eine "private" Funktion umgestaltet werden.

gibt es ein allgemeines Muster für den Umgang mit solchen Szenarien?

Ich glaube nicht, dass es ein "Muster" (im Sinne der GoF-Entwurfsmuster) gibt, das ein gutes API-Design beschreibt. Die Verwendung selbstbeschreibender Namen, Funktionen mit weniger Parametern und Funktionen mit orthogonalen (= unabhängigen) Parametern sind nur Grundprinzipien für die Erstellung von lesbarem, wartbarem und weiterentwickelbarem Code. Nicht jede gute Idee in der Programmierung ist notwendigerweise ein "Entwurfsmuster".

99
Doc Brown

Außerdem habe ich das Gefühl, dass dies einen schlechten Präzedenzfall darstellt und der API Verantwortlichkeiten hinzufügt, mit denen es sich nicht befassen sollte (d. H. SRP verletzt). Angenommen, zusätzliche Benutzer möchten, dass die Funktion mehr Anwendungsfälle unterstützt, z. B. die Berechnung von total aus den Parametern numMonths und monthlyPayment. Diese Funktion wird mit der Zeit immer komplizierter.

Du bist genau richtig.

Ich bevorzuge es, die Funktion so zu belassen, wie sie war, und stattdessen den Aufrufer zu verpflichten, das Enddatum selbst zu berechnen. Ich kann mich jedoch irren und habe mich gefragt, ob die vorgenommenen Änderungen eine akzeptable Möglichkeit zum Entwerfen einer API-Funktion darstellen.

Dies ist auch nicht ideal, da der Anrufercode mit einer nicht verwandten Kesselplatte verschmutzt wird.

Gibt es alternativ ein allgemeines Muster für den Umgang mit solchen Szenarien?

Führen Sie einen neuen Typ wie DateInterval ein. Fügen Sie alle sinnvollen Konstruktoren hinzu (Startdatum + Enddatum, Startdatum + Anzahl Monate, was auch immer). Verwenden Sie dies als gängige Währungstypen, um Datums-/Uhrzeitintervalle in Ihrem System auszudrücken.

Manchmal helfen dabei fließende Ausdrücke:

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

Wenn Sie genügend Zeit für das Design haben, können Sie eine solide API entwickeln, die einer domänenspezifischen Sprache ähnelt.

Der andere große Vorteil ist, dass IDEs mit automatischer Vervollständigung das Lesen der API-Dokumentation fast unwiderruflich machen, da dies aufgrund ihrer selbsterkennbaren Funktionen intuitiv ist.

Es gibt Ressourcen wie https://nikas.praninskas.com/javascript/2015/04/26/fluent-javascript/ oder https://github.com/nikaspran /fluent.js zu diesem Thema.

Beispiel (aus dem ersten Ressourcenlink):

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

Nun, in anderen Sprachen würden Sie benannte Parameter verwenden. Dies kann in Javascript emuliert werden:

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

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

Alternativ können Sie auch die Verantwortung für die Angabe der Anzahl der Monate aufheben und diese aus Ihrer Funktion herausnehmen:

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

Und der getpaymentBreakdown würde ein Objekt erhalten, das die Basisanzahl der Monate liefert

Diese würden eine Funktion höherer Ordnung zum Beispiel eine Funktion zurückgeben.

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

Und wenn Sie mit einem System mit diskriminierten Gewerkschaften/algebraischen Datentypen arbeiten, können Sie es beispielsweise als TimePeriodSpecification übergeben.

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

und dann würde keines der Probleme auftreten, bei denen Sie eines nicht implementieren könnten und so weiter.

0
NiklasJ