Nous avons une fonction API qui décompose un montant total en montants mensuels en fonction des 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 pour cette API a voulu spécifier la plage de dates d'autres manières: 1) en fournissant 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 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. L'appelant doit maintenant se soucier des heuristiques cachées avec l'implémentation de la fonction pour déterminer quels paramètres sont préférés pour être utilisés pour calculer la plage de dates (c'est-à-dire par ordre de priorité monthlyPayment
, numMonths
, endDate
). Si un appelant ne fait pas attention à la signature de la fonction, il peut envoyer plusieurs des paramètres facultatifs et se demander pourquoi endDate
est ignoré. Nous spécifions ce comportement dans la documentation de la fonction.
De plus, je pense que cela crée un mauvais précédent et ajoute des responsabilités à l'API dont il ne devrait pas se préoccuper (c'est-à-dire violer SRP). Supposons que des consommateurs supplémentaires souhaitent que la fonction prenne en charge davantage de cas d'utilisation, tels que le calcul de 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 conserver la fonction telle qu'elle était et d'exiger à la place que l'appelant calcule endDate
lui-même. Cependant, je peux me tromper et je me demandais si les changements qu'ils avaient apportés é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 des fonctions supplémentaires d'ordre supérieur dans notre API 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 à l'intérieur de la fonction.
En voyant l'implémentation, il me semble que ce dont vous avez vraiment besoin ici, c'est de 3 fonctions différentes au lieu d'une:
L'original:
function getPaymentBreakdown(total, startDate, endDate)
Celui qui indique le nombre de mois au lieu de la date de fin:
function getPaymentBreakdownByNoOfMonths(total, startDate, noOfMonths)
et celui fournissant le paiement mensuel et calculant la date de fin:
function getPaymentBreakdownByMonthlyPayment(total, startDate, monthlyPayment)
Maintenant, il n'y a plus de paramètres optionnels, et il devrait être assez clair 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 n'obscurcirait 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, il doit être refactorisé en fonction "privée".
existe-t-il un modèle commun pour gérer des scénarios comme celui-ci
Je ne pense pas qu'il existe un "modèle" (dans le sens des modèles de conception GoF) qui décrit une bonne conception d'API. Utiliser des noms auto-descriptifs, des fonctions avec moins de paramètres, des fonctions avec des paramètres orthogonaux (= indépendants), ne sont que des principes de base pour créer du code lisible, maintenable et évolutif. Toutes les bonnes idées en programmation ne sont pas nécessairement un "modèle de conception".
De plus, je pense que cela crée un mauvais précédent et ajoute des responsabilités à l'API dont il ne devrait pas se préoccuper (c'est-à-dire violer SRP). Supposons que des consommateurs supplémentaires souhaitent que la fonction prenne en charge davantage de cas d'utilisation, tels que le calcul de
total
à partir des paramètresnumMonths
etmonthlyPayment
. Cette fonction deviendra de plus en plus compliquée avec le temps.
Vous avez exactement raison.
Ma préférence est de conserver la fonction telle qu'elle était et d'exiger que l'appelant calcule lui-même la date de fin. Cependant, je peux me tromper et je me demandais si les changements qu'ils avaient apportés étaient un moyen acceptable de concevoir une fonction API.
Ce n'est pas idéal non plus, car le code de l'appelant sera pollué par une plaque de chaudière indépendante.
Sinon, existe-t-il un modèle commun pour gérer des scénarios comme celui-ci?
Introduisez un nouveau type, comme DateInterval
. Ajoutez les constructeurs qui ont du sens (date de début + date de fin, date de début + nombre de mois, peu importe.). Adoptez-le comme types de devise commune pour exprimer les intervalles de dates/heures dans votre système.
Parfois, les expressions fluides aident à cela:
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();
Si vous disposez de suffisamment de temps pour concevoir, vous pouvez créer une API solide qui agit de manière similaire à un langage spécifique au domaine.
L'autre gros avantage est que les IDE avec la saisie semi-automatique rendent presque peu pratique la lecture de la documentation de l'API, tout comme intuitif en raison de ses capacités auto-détectables.
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):
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]
Eh bien, dans d'autres langues, vous utiliseriez paramètres nommés. Cela peut être émulé en Javascript:
function getPaymentBreakdown(total, startDate, durationSpec) { ... }
getPaymentBreakdown(100, today, {endDate: whatever});
getPaymentBreakdown(100, today, {noOfMonths: 4});
getPaymentBreakdown(100, today, {monthlyPayment: 20});
Comme alternative, vous pouvez également vous décharger de la responsabilité de spécifier le nombre de mois et le laisser hors de votre fonction:
getPaymentBreakdown(420, numberOfMonths(3))
getPaymentBreakdown(420, dateRage(a, b))
getPaymentBreakdown(420, paymentAmount(350))
Et le getpaymentBreakdown recevrait un objet qui fournirait le nombre de mois de base
Ceux-ci auraient une fonction d'ordre supérieur renvoyant par exemple une fonction.
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)
};
}
Et si vous deviez travailler avec un système avec des unions/types de données algébriques discriminés, vous pourriez le transmettre comme, disons, un TimePeriodSpecification
.
type TimePeriodSpecification =
| DateRange of startDate : DateTime * endDate : DateTime
| MonthCount of startDate : DateTime * monthCount : int
| MonthlyPayment of startDate : DateTime * monthlyAmount : float
et puis aucun des problèmes ne se produirait si vous ne parveniez pas à en implémenter un et ainsi de suite.