Existe-t-il un modèle de traitement des paramètres de fonction en conflit?

38

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 endDateest 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 à totalpartir des paramètres numMonthset 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- endDatemê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.

CalMlynarczyk
la source
79
"Récemment, un consommateur de cette API a voulu [fournir] le nombre de mois au lieu de la date de fin" - Il s'agit d'une demande frivole. Ils peuvent transformer le nombre de mois en une date de fin appropriée en une ou deux lignes de code à leur fin.
Graham
12
qui ressemble à un anti-pattern Flag Argument, et je recommanderais également de scinder en plusieurs fonctions
njzk2
2
Comme une note de côté, il y a des fonctions qui peuvent accepter le même type et le nombre de paramètres et de produire des résultats très différents en fonction de ceux - voir 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 à Datenouveau 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.
VLAZ
Sur une légère tangente, vous voudrez peut-être réfléchir à la façon de traiter le cas où monthlyPaymentest donné mais totaln'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 avec total = 0.3et monthlyPayment = 0.1).
Ilmari Karonen le
@ Graham Je n'ai pas réagi à cela ... J'ai réagi à la déclaration suivante "En réponse à cela, l'équipe de l'API a changé la fonction ..." - se met en position fœtale et commence à basculer - Peu importe où cette ligne ou deux du code va, soit un nouvel appel d'API avec un format différent, soit effectué du côté de l'appelant. Il suffit de ne pas changer un appel API de travail comme celui-ci!
Baldrickk le

Réponses:

99

En voyant l’implémentation, il me semble que vous avez vraiment besoin de 3 fonctions différentes au lieu d’une:

L'original:

function getPaymentBreakdown(total, startDate, endDate) 

Celui qui fournit le nombre de mois au lieu de la date de fin:

function getPaymentBreakdownByNoOfMonths(total, startDate, noOfMonths) 

et celui qui fournit le paiement mensuel et calcule la date de fin:

function getPaymentBreakdownByMonthlyPayment(total, startDate, monthlyPayment) 

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".

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" (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".

Doc Brown
la source
24
En réalité, l’implémentation "commune" du code pourrait simplement être 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?
Giacomo Alzetta le
@GiacomoAlzetta: c'est possible. Mais je suis à peu près sûr que la mise en œuvre deviendra plus simple en fournissant une fonction commune qui ne contient que la partie "retour" de la fonction OP, et en laissant les fonctions publiques 3 appeler cette fonction avec les paramètres innerNumMonths, totalet startDate. 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?
Doc Brown le
3
Je ne voulais pas dire "conserve la fonction 5 arguments". Je dis simplement que lorsque vous avez une logique commune, cette logique n’est pas nécessairement privée . Dans ce cas, les 3 fonctions peuvent être refactorisées pour simplement transformer les paramètres en dates de début / fin. Vous pouvez ainsi utiliser la 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.
Giacomo Alzetta le
@GiacomoAlzetta: ok, était un malentendu, je pensais que vous parliez de la deuxième mise getPaymentBreakdownen œuvre de dans la question.
Doc Brown le
J'irais jusqu'à ajouter une nouvelle version de la méthode d'origine explicitement appelée "getPaymentBreakdownByStartAndEnd" et déconseiller la méthode d'origine si vous souhaitez toutes les fournir.
Erik
20

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 à totalpartir des paramètres numMonthset monthlyPayment. Cette fonction deviendra de plus en plus compliquée avec le temps.

Vous avez tout à fait raison.

Ma préférence est de garder la fonction telle qu'elle était et de demander à l'appelant de calculer lui-même la date de fin. Cependant, je peux me tromper et je me demandais si les modifications apportées é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 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.

Alexander - Rétablir Monica
la source
3
@DocBrown Yep. Dans de tels cas (Ruby, Python, JS), il est d'usage d'utiliser uniquement des méthodes static / class. Mais c’est un détail d’implémentation qui, à mon avis, n’est pas particulièrement pertinent pour ma réponse ("utiliser un type").
Alexander - Réintégrer Monica le
2
Et cette idée atteint malheureusement ses limites avec la troisième condition: date de début, paiement total et paiement mensuel - et la fonction calculera le DateInterval à partir des paramètres monétaires - et vous ne devez pas mettre les montants dans votre plage de dates ...
Falco
3
@DocBrown "déplace uniquement le problème de la fonction existante au constructeur de type" Oui, cela met le code temporel à l'endroit où le code temporel doit aller, de sorte que le code monétaire peut être le même que le code monétaire. C'est un SRP simple, donc je ne suis pas sûr de savoir où vous voulez en venir quand vous dites que "seulement" déplace le problème. C'est ce que font toutes les fonctions. Ils ne font pas disparaître le code, ils le déplacent dans des endroits plus appropriés. Quel est votre problème avec ça? "mais mes félicitations, au moins 5 votants ont mordu à l'hameçon" Cela semble beaucoup plus con que ce que je pensais (espérer).
Alexander - Réintégrer Monica le
@Falco Cela ressemble à une nouvelle méthode pour moi (sur cette classe de calculatrice de paiement, pas DateInterval):calculatePayPeriod(startData, totalPayment, monthlyPayment)
Alexander - Réintégrer Monica
7

Parfois, les expressions fluides aident sur ceci:

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 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):

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]
DanielCuadra
la source
8
Fluent interface en elle - même ne fait aucune tâche particulière plus ou moins difficile. Cela ressemble plus au motif Builder.
VLAZ
8
La mise en œuvre serait plutôt compliquée, toutefois, si vous devez éviter les appels erronés tels queforTotalAmount(1234).breakIntoPayments().byPeriod(2).monthly().withPaymentsOf(12.34).byDateRange(saleStart, saleEnd);
Bergi
4
Si les développeurs veulent vraiment se prendre en main, il existe des moyens plus simples @Bergi. Néanmoins, l’exemple que vous avez donné est bien plus lisible queforTotalAmountAndBreakIntoPaymentsByPeriodThenMonthlyWithPaymentsOfButByDateRange(1234, 2, 12.34, saleStart, saleEnd);
DanielCuadra
5
@DanielCuadra Ce que j'essayais de dire, c'est que votre réponse ne résout pas vraiment le problème des PO qui consiste à avoir 3 paramètres mutuellement exclusifs. L'utilisation du modèle de générateur peut rendre l'appel plus lisible (et augmenter la probabilité que l'utilisateur remarque que cela n'a aucun sens), mais l'utilisation du modèle de générateur seul ne l'empêche pas de continuer à transmettre 3 valeurs à la fois.
Bergi le
2
@ Falco le fera-t-il? Oui, c'est possible, mais plus compliqué, et la réponse n'en faisait aucune mention. Les constructeurs les plus communs que j'ai vus consistaient en une seule classe. Si la réponse est modifiée pour inclure le code du ou des constructeurs, je l'approuverai volontiers et supprimerai mon vote négatif.
Bergi le
2

Dans d’autres langues, vous utiliseriez des paramètres nommés . Ceci peut être imité dans Javscript:

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

getPaymentBreakdown(100, today, {endDate: whatever});
getPaymentBreakdown(100, today, {noOfMonths: 4});
getPaymentBreakdown(100, today, {monthlyPayment: 20});
Gregory Currie
la source
6
Comme le modèle de générateur ci-dessous, cela rend l'appel plus lisible (et augmente la probabilité que l'utilisateur remarque que cela n'a pas de sens), mais nommer les paramètres n'empêche pas l'utilisateur de toujours transmettre 3 valeurs à la fois - par exemple getPaymentBreakdown(100, today, {endDate: whatever, noOfMonths: 4, monthlyPayment: 20}).
Bergi le
1
Ça ne devrait pas être à la :place de =?
Barmar le
J'imagine que vous pourriez vérifier qu'un seul des paramètres est non nul (ou ne figure pas dans le dictionnaire).
Mateen Ulhaq le
1
@Bergi - La syntaxe en elle-même n'empêche pas les utilisateurs de transmettre des paramètres non-sens mais vous pouvez simplement effectuer des validations et jeter des erreurs
slebetman
@ Bergi Je ne suis en aucun cas un expert en Javascript, mais je pense que l'attribution de la destruction dans ES6 peut aider ici, bien que je sois très léger sur le sujet.
Gregory Currie le
1

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:

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 seraient fonction d'ordre supérieur retournant 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)
  };
}
Vinz243
la source
Qu'est-il arrivé aux paramètres totalet startDate?
Bergi le
Cela semble être une bonne API, mais pourriez-vous ajouter comment vous imaginez que ces quatre fonctions seront implémentées? (Avec des types de variantes et une interface commune, cela pourrait être assez élégant, mais ce que vous aviez en tête n'est pas clair).
Bergi le
@Bergi a édité mon post
Vinz243 le
0

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.

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

et alors aucun des problèmes ne se produirait où vous pourriez échouer à en implémenter un et ainsi de suite.

NiklasJ
la source
C’est définitivement ainsi que j’aborderais cette question dans une langue qui propose des types comme ceux-ci. J'ai essayé de garder ma question agnostique, mais il faudrait peut-être tenir compte du langage utilisé, car de telles approches deviennent possibles dans certains cas.
CalMlynarczyk le