Nous avons une fonction API qui décompose un montant total en montants mensuels en fonction de dates de début et de fin données.
// 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,
};
}
Récemment, un consommateur de cette API a voulu spécifier la plage de dates de différentes manières: 1) en indiquant le nombre de mois au lieu de la date de fin, ou 2) en fournissant le paiement mensuel et en calculant la date de fin. En réponse à cela, l'équipe de l'API a modifié la fonction comme suit:
// 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),
};
}
Je pense que ce changement complique l'API. Maintenant , l'appelant a besoin de se soucier de la heuristiques cachés avec la mise en œuvre de la fonction pour déterminer quels paramètres prennent la préférence à être utilisé pour calculer la plage de dates (par exemple par ordre de priorité monthlyPayment
, numMonths
, endDate
). Si un appelant ne prête pas attention à la signature de la fonction, il peut envoyer plusieurs des paramètres facultatifs et se demander pourquoi il endDate
est ignoré. Nous spécifions ce comportement dans la documentation de la fonction.
De plus, j'estime que cela crée un mauvais précédent et ajoute des responsabilités à l'API avec lesquelles elle ne devrait pas se préoccuper (c.-à-d. Violer SRP). Supposons que des consommateurs supplémentaires souhaitent que la fonction prenne en charge davantage de cas d'utilisation, tels que le calcul à total
partir des paramètres numMonths
et monthlyPayment
. Cette fonction deviendra de plus en plus compliquée avec le temps.
Ma préférence est de garder la fonction telle quelle et d'exiger que l'appelant calcule lui- endDate
même. Cependant, je peux me tromper et je me demandais si les modifications apportées étaient un moyen acceptable de concevoir une fonction API.
Sinon, existe-t-il un modèle commun pour gérer des scénarios comme celui-ci? Nous pourrions fournir dans notre API des fonctions supplémentaires d'ordre supérieur qui encapsulent la fonction d'origine, mais cela alourdit l'API. Nous pourrions peut-être ajouter un paramètre indicateur supplémentaire spécifiant quelle approche utiliser dans la fonction.
la source
Date
- vous pouvez fournir une chaîne et il peut être analysé pour déterminer la date. Cependant, de cette façon, les paramètres de manipulation peuvent également être très pointilleux et peuvent produire des résultats peu fiables. Voir àDate
nouveau Il n’est pas impossible de bien faire les choses - Moment le gère beaucoup mieux, mais c’est très agaçant de l’utiliser quand même.monthlyPayment
est donné maistotal
n'est pas un multiple entier de celui-ci. Et aussi comment traiter d'éventuelles erreurs d'arrondi en virgule flottante s'il n'est pas garanti que les valeurs soient des entiers (par exemple, essayez avectotal = 0.3
etmonthlyPayment = 0.1
).Réponses:
En voyant l’implémentation, il me semble que vous avez vraiment besoin de 3 fonctions différentes au lieu d’une:
L'original:
Celui qui fournit le nombre de mois au lieu de la date de fin:
et celui qui fournit le paiement mensuel et calcule la date de fin:
Maintenant, il n’ya plus de paramètres optionnels, et il devrait être assez clair de savoir quelle fonction est appelée comment et dans quel but. Comme mentionné dans les commentaires, dans un langage strictement typé, on pourrait également utiliser la surcharge de fonctions, en distinguant les 3 fonctions différentes non pas nécessairement par leur nom, mais par leur signature, au cas où cela ne dissimule pas leur objectif.
Notez que les différentes fonctions ne signifient pas que vous devez dupliquer une logique. En interne, si ces fonctions partagent un algorithme commun, celui-ci doit être remodelé pour devenir une fonction "privée".
Je ne pense pas qu'il existe un "modèle" (au sens des modèles de conception GoF) décrivant une bonne conception d'API. L'utilisation de noms auto-descriptifs, de fonctions avec moins de paramètres, de fonctions avec des paramètres orthogonaux (= indépendants), ne sont que des principes de base pour la création de code lisible, maintenable et évolutive. Toutes les bonnes idées en programmation ne sont pas nécessairement un "modèle de conception".
la source
getPaymentBreakdown
(ou en réalité l’un de ces 3) et les deux autres fonctions ne feraient que convertir les arguments et appeler cela. Pourquoi ajouter une fonction privée qui est une copie parfaite de l’un de ces 3?innerNumMonths
,total
etstartDate
. Pourquoi garder une fonction trop compliquée avec 5 paramètres, où 3 sont presque facultatifs (sauf qu’un doit être défini), alors qu’une fonction à 3 paramètres fera également le travail?getPaymentBreakdown(total, startDate, endDate)
fonction publique comme implémentation commune. L'autre outil calculera simplement les dates de début / fin / fin appropriées et l'appellera.getPaymentBreakdown
en œuvre de dans la question.Vous avez tout à fait raison.
Ce n'est pas idéal non plus, car le code de l'appelant sera pollué par une plaque de chaudière indépendante.
Introduisez un nouveau type, comme
DateInterval
. Ajoutez ce que les constructeurs ont de sens (date de début + date de fin, date de début + num mois, peu importe.). Adoptez-le comme types de devise commune pour exprimer des intervalles de dates / heures dans votre système.la source
DateInterval
):calculatePayPeriod(startData, totalPayment, monthlyPayment)
Parfois, les expressions fluides aident sur ceci:
Si vous avez suffisamment de temps pour concevoir, vous pouvez créer une API solide qui ressemble à un langage spécifique à un domaine.
L'autre grand avantage est que les IDE avec la saisie semi- automatique rendent la lecture de la documentation de l'API presque irréprochable, de manière intuitive en raison de ses capacités d'auto-découverte.
Il existe des ressources telles que https://nikas.praninskas.com/javascript/2015/04/26/fluent-javascript/ ou https://github.com/nikaspran/fluent.js sur ce sujet.
Exemple (tiré du premier lien de ressource):
la source
forTotalAmount(1234).breakIntoPayments().byPeriod(2).monthly().withPaymentsOf(12.34).byDateRange(saleStart, saleEnd);
forTotalAmountAndBreakIntoPaymentsByPeriodThenMonthlyWithPaymentsOfButByDateRange(1234, 2, 12.34, saleStart, saleEnd);
Dans d’autres langues, vous utiliseriez des paramètres nommés . Ceci peut être imité dans Javscript:
la source
getPaymentBreakdown(100, today, {endDate: whatever, noOfMonths: 4, monthlyPayment: 20})
.:
place de=
?Au lieu de cela, vous pouvez également vous décharger de la responsabilité de spécifier le nombre de mois et de le laisser en dehors de votre fonction:
Et le getpaymentBreakdown recevrait un objet qui fournirait le nombre de mois de base
Ceux-ci seraient fonction d'ordre supérieur retournant par exemple une fonction.
la source
total
etstartDate
?Et si vous travailliez avec un système avec des types de données discriminants sur les syndicats / algébriques, vous pourriez le transmettre sous la forme: a
TimePeriodSpecification
.et alors aucun des problèmes ne se produirait où vous pourriez échouer à en implémenter un et ainsi de suite.
la source