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. In Reaktion darauf hat das API-Team die Funktion folgendermaßen geändert:
// 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 glaube, diese Änderung erschwert die API. Nun muss der Anrufer Sorge um die versteckte Heuristik mit der Umsetzung der Funktion bei der Bestimmung , welche Parameter nehmen Präferenz verwendet wird , um den Datumsbereich (dh nach der Reihenfolge ihrer Priorität zu berechnen monthlyPayment
, numMonths
, endDate
). Wenn ein Aufrufer die Funktionssignatur nicht beachtet, sendet er möglicherweise mehrere der optionalen Parameter und ist verwirrt, warum endDate
diese ignoriert werden. Dieses Verhalten legen wir in der Funktionsdokumentation fest.
Außerdem habe ich das Gefühl, dass dies einen schlechten Präzedenzfall darstellt und der API Verantwortlichkeiten hinzufügt, mit denen sie sich nicht befassen sollte (dh die SRP verletzen). Angenommen, zusätzliche Verbraucher möchten, dass die Funktion mehr Anwendungsfälle unterstützt, z. B. das Berechnen 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 Anrufer aufzufordern, sich 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. Möglicherweise könnten wir einen zusätzlichen Flag-Parameter hinzufügen, der angibt, welcher Ansatz innerhalb der Funktion verwendet werden soll.
quelle
Date
liefern können. Sie können eine Zeichenfolge angeben und sie kann analysiert werden, um das Datum zu bestimmen. Auf diese Weise können die Handhabungsparameter jedoch auch sehr schwierig sein und zu unzuverlässigen Ergebnissen führen. Sehen Sie nochDate
einmal. Es ist nicht unmöglich, das Richtige zu tun - Moment handhabt es viel besser, aber es ist sehr ärgerlich, es trotzdem zu benutzen.monthlyPayment
gegebenen Fall umgehen sollen, der jedochtotal
kein ganzzahliges Vielfaches davon ist. Und auch, wie man mit möglichen Gleitkomma-Rundungsfehlern umgeht, wenn die Werte nicht garantiert ganze Zahlen sind (z. B. probieren Sie es mittotal = 0.3
und ausmonthlyPayment = 0.1
).Antworten:
In Anbetracht der Implementierung scheint es mir, dass Sie hier wirklich drei verschiedene Funktionen anstelle von einer benötigen:
Das Original:
Derjenige, der die Anzahl der Monate anstelle des Enddatums angibt:
und derjenige, der die monatliche Zahlung vornimmt und das Enddatum berechnet:
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 getippten Sprache auch eine Funktionsüberladung verwenden, die die drei verschiedenen Funktionen nicht unbedingt anhand ihres Namens, sondern anhand ihrer Signatur unterscheidet, falls dies ihren Zweck nicht verschleiert.
Beachten Sie, dass die verschiedenen Funktionen nicht bedeuten, dass Sie eine Logik duplizieren müssen. Wenn diese Funktionen intern einen gemeinsamen Algorithmus verwenden, sollte dieser in eine "private" Funktion umgestaltet werden.
Ich glaube nicht, dass es ein "Muster" (im Sinne der GoF-Designmuster) 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 entwickelbarem Code. Nicht jede gute Idee in der Programmierung ist notwendigerweise ein "Entwurfsmuster".
quelle
getPaymentBreakdown
(oder wirklich eine dieser 3) und die anderen beiden Funktionen konvertieren einfach die Argumente und nennen das. Warum sollte eine private Funktion hinzugefügt werden, die eine perfekte Kopie dieser drei Funktionen ist?innerNumMonths
,total
undstartDate
. Warum sollte eine überkomplizierte Funktion mit 5 Parametern beibehalten werden, wobei 3 fast optional sind (außer einer muss eingestellt werden), wenn auch eine 3-Parameter-Funktion die Aufgabe übernimmt?getPaymentBreakdown(total, startDate, endDate)
Funktion als allgemeine Implementierung verwenden. Das andere Tool berechnet einfach die geeigneten Gesamt- / Start- / Enddaten und ruft sie auf.getPaymentBreakdown
in der frage .Du bist genau richtig.
Dies ist auch nicht ideal, da der Anrufercode durch nicht zugehörige Kesselschilder verschmutzt wird.
Führe einen neuen Typ ein, wie
DateInterval
. Fügen Sie die Konstruktoren hinzu, die Sinn machen (Startdatum + Enddatum, Startdatum + Anzahl Monate, was auch immer). Übernehmen Sie dies als die gängigen Währungstypen für die Angabe von Datums- / Uhrzeitintervallen in Ihrem System.quelle
DateInterval
):calculatePayPeriod(startData, totalPayment, monthlyPayment)
Manchmal helfen dabei fließende Ausdrücke:
Wenn Sie genügend Zeit zum Entwerfen haben, können Sie eine solide API entwickeln, die einer domänenspezifischen Sprache ähnelt.
Der andere große Vorteil besteht darin, dass IDEs mit automatischer Vervollständigung das Lesen der API-Dokumentation fast unangenehm machen , da sie aufgrund ihrer selbsterkennbaren Funktionen intuitiv sind.
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 (entnommen aus dem ersten Ressourcenlink):
quelle
forTotalAmount(1234).breakIntoPayments().byPeriod(2).monthly().withPaymentsOf(12.34).byDateRange(saleStart, saleEnd);
forTotalAmountAndBreakIntoPaymentsByPeriodThenMonthlyWithPaymentsOfButByDateRange(1234, 2, 12.34, saleStart, saleEnd);
In anderen Sprachen würden Sie benannte Parameter verwenden . Dies kann in Javscript emuliert werden:
quelle
getPaymentBreakdown(100, today, {endDate: whatever, noOfMonths: 4, monthlyPayment: 20})
.:
statt sein=
?Alternativ können Sie auch die Verantwortung für die Angabe der Monatsanzahl aufheben und diese aus Ihrer Funktion ausschließen:
Und getpaymentBreakdown würde ein Objekt erhalten, das die Basisanzahl der Monate liefert
Diese würden eine Funktion höherer Ordnung zurückgeben, zum Beispiel eine Funktion.
quelle
total
und passiertstartDate
?Und wenn Sie mit einem System mit diskriminierten Gewerkschaften / algebraischen Datentypen arbeiten würden, könnten Sie es wie folgt übergeben: a
TimePeriodSpecification
.und dann würde keines der Probleme auftreten, wenn Sie eines nicht implementieren könnten und so weiter.
quelle