Esiste un modello per la gestione di parametri di funzioni in conflitto?


38

Abbiamo una funzione API che suddivide un importo totale in importi mensili in base alle date di inizio e fine indicate.

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

Di recente, un consumatore per questa API ha voluto specificare l'intervallo di date in altri modi: 1) fornendo il numero di mesi anziché la data di fine, oppure 2) fornendo il pagamento mensile e calcolando la data di fine. In risposta a ciò, il team API ha modificato la funzione nel modo seguente:

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

Sento che questo cambiamento complica l'API. Ora il chiamante ha bisogno di preoccuparsi per l'euristica nascosti con l'implementazione della funzione nel determinare quali parametri prendono preferenza nell'essere utilizzato per calcolare l'intervallo di date (ad esempio in ordine di priorità monthlyPayment, numMonths, endDate). Se un chiamante non presta attenzione alla firma della funzione, potrebbe inviare più parametri opzionali e confondersi sul motivo per cui endDateviene ignorato. Specifichiamo questo comportamento nella documentazione della funzione.

Inoltre, ritengo che costituisca un precedente negativo e aggiunga all'API delle responsabilità che non dovrebbe riguardare (ovvero la violazione di SRP). Supponiamo che altri utenti desiderino che la funzione supporti più casi d'uso, come il calcolo totaldai parametri numMonthse monthlyPayment. Questa funzione diventerà sempre più complicata nel tempo.

La mia preferenza è mantenere la funzione com'era e invece richiedere al chiamante di calcolarsi endDate. Tuttavia, potrei sbagliarmi e mi chiedevo se le modifiche apportate fossero un modo accettabile per progettare una funzione API.

In alternativa, esiste un modello comune per la gestione di scenari come questo? Potremmo fornire ulteriori funzioni di ordine superiore nella nostra API che racchiudono la funzione originale, ma questo gonfia l'API. Forse potremmo aggiungere un parametro flag aggiuntivo specificando quale approccio usare all'interno della funzione.


79
"Di recente, un consumatore per questa API ha voluto [fornire] il numero di mesi anziché la data di fine" - Questa è una richiesta frivola. Possono trasformare il numero di mesi in una data di fine corretta in una riga o due di codice alla fine.
Graham,

12
che sembra un anti-pattern di Flag Argument, e consiglierei anche di suddividere in diverse funzioni
njzk2

2
Come nota a margine, ci sono funzioni che possono accettare lo stesso tipo e numero di parametri e produrre risultati molto diversi in base a quelli - vedi Date- è possibile fornire una stringa e può essere analizzata per determinare la data. Tuttavia, in questo modo i parametri di gestione possono anche essere molto delicati e potrebbero produrre risultati inaffidabili. Vedi di Datenuovo Non è impossibile fare la cosa giusta: Moment la gestisce meglio, ma è comunque molto fastidiosa da usare.
VLAZ,

Su una leggera tangente, potresti voler pensare a come gestire il caso in cui monthlyPaymentè dato ma totalnon è un multiplo intero di esso. E anche come gestire eventuali errori di arrotondamento in virgola mobile se i valori non sono garantiti come numeri interi (ad esempio, provalo con total = 0.3e monthlyPayment = 0.1).
Ilmari Karonen,

@Graham Non ho reagito a questo ... Ho reagito alla prossima affermazione "In risposta a questo, il team API ha cambiato la funzione ..." - si sposta in posizione fetale e inizia a dondolare - Non importa dove quella riga o due di codice vanno, o una nuova chiamata API con il formato diverso, o eseguita alla fine del chiamante. Basta non cambiare una chiamata API funzionante come questa!
Baldrickk,

Risposte:


99

Vedendo l'implementazione, mi sembra che ciò di cui hai veramente bisogno qui siano 3 diverse funzioni invece di una:

Quello originale:

function getPaymentBreakdown(total, startDate, endDate) 

Quello che fornisce il numero di mesi anziché la data di fine:

function getPaymentBreakdownByNoOfMonths(total, startDate, noOfMonths) 

e quello che fornisce il pagamento mensile e calcola la data di fine:

function getPaymentBreakdownByMonthlyPayment(total, startDate, monthlyPayment) 

Ora non ci sono più parametri opzionali e dovrebbe essere abbastanza chiaro quale funzione viene chiamata come e per quale scopo. Come menzionato nei commenti, in un linguaggio strettamente tipizzato, si potrebbe anche utilizzare il sovraccarico delle funzioni, distinguendo le 3 diverse funzioni non necessariamente dal loro nome, ma dalla loro firma, nel caso in cui ciò non offuschi il loro scopo.

Nota le diverse funzioni non significano che devi duplicare qualsiasi logica - internamente, se queste funzioni condividono un algoritmo comune, dovrebbe essere refactored a una funzione "privata".

esiste un modello comune per la gestione di scenari come questo

Non credo che esista un "modello" (nel senso dei modelli di progettazione GoF) che descrive una buona progettazione delle API. L'uso di nomi che descrivono se stessi, funzioni con meno parametri, funzioni con parametri ortogonali (= indipendenti), sono solo principi di base per la creazione di codice leggibile, mantenibile ed evolvibile. Non tutte le buone idee in programmazione sono necessariamente un "modello di progettazione".


24
In realtà l'implementazione "comune" del codice potrebbe essere semplicemente getPaymentBreakdown(o davvero una qualsiasi di quelle 3) e le altre due funzioni convertono semplicemente gli argomenti e lo chiamano. Perché aggiungere una funzione privata che è una copia perfetta di uno di questi 3?
Giacomo Alzetta,

@GiacomoAlzetta: è possibile. Ma sono abbastanza sicuro che l'implementazione diventerà più semplice fornendo una funzione comune che contiene solo la parte "return" della funzione OP, e lascia che le 3 funzioni pubbliche chiamino questa funzione con parametri innerNumMonths, totale startDate. Perché mantenere una funzione complicata con 5 parametri, dove 3 sono quasi opzionali (tranne uno deve essere impostato), quando anche una funzione a 3 parametri farà il lavoro?
Doc Brown,

3
Non intendevo dire "mantieni la funzione 5 argomento". Sto solo dicendo che quando hai una logica comune questa logica non deve essere privata . In questo caso, tutte e 3 le funzioni possono essere rifattorizzate per trasformare semplicemente i parametri in date di inizio-fine, in modo da poter utilizzare la getPaymentBreakdown(total, startDate, endDate)funzione pubblica come implementazione comune, l'altro strumento calcolerà semplicemente le date di totale / inizio / fine appropriate e la chiamerà.
Giacomo Alzetta,

@GiacomoAlzetta: ok, è stato un malinteso, pensavo che parlassi della seconda implementazione della getPaymentBreakdowndomanda.
Doc Brown,

Andrei fino all'aggiunta di una nuova versione del metodo originale che è esplicitamente chiamato "getPaymentBreakdownByStartAndEnd" e deprecando il metodo originale, se si desidera fornire tutti questi.
Erik

20

Inoltre, ritengo che costituisca un precedente negativo e aggiunga all'API delle responsabilità che non dovrebbe riguardare (ovvero la violazione di SRP). Supponiamo che altri utenti desiderino che la funzione supporti più casi d'uso, come il calcolo totaldai parametri numMonthse monthlyPayment. Questa funzione diventerà sempre più complicata nel tempo.

Hai esattamente ragione.

La mia preferenza è mantenere la funzione com'era e invece richiedere al chiamante di calcolare endDate da soli. Tuttavia, potrei sbagliarmi e mi chiedevo se le modifiche apportate fossero un modo accettabile per progettare una funzione API.

Anche questo non è l'ideale, perché il codice chiamante verrà inquinato con una piastra caldaia non correlata.

In alternativa, esiste un modello comune per la gestione di scenari come questo?

Introdurre un nuovo tipo, ad esempio DateInterval. Aggiungi qualunque costruttore abbia senso (data di inizio + data di fine, data di inizio + num mesi, qualunque sia.). Adottare questo come tipi di valuta comune per esprimere intervalli di date / orari in tutto il sistema.


3
@DocBrown Yep. In questi casi (Ruby, Python, JS), è consuetudine utilizzare solo metodi statici / di classe. Ma questo è un dettaglio di implementazione, che non credo sia particolarmente rilevante per il punto della mia risposta ("usa un tipo").
Alexander - Ripristina Monica il

2
E questa idea purtroppo raggiunge i suoi limiti con il terzo requisito: data di inizio, pagamento totale e un pagamento mensile - e la funzione calcolerà il DateInterval dai parametri del denaro - e non dovresti inserire gli importi monetari nell'intervallo di date ...
Falco,

3
@DocBrown "sposta il problema solo dalla funzione esistente al costruttore del tipo" Sì, sta mettendo il time code dove dovrebbe andare il time code, in modo che il codice denaro possa essere dove dovrebbe andare il codice denaro. È un semplice SRP, quindi non sono sicuro di cosa stai arrivando quando lo dici "solo" sposta il problema. Ecco cosa fanno tutte le funzioni. Non fanno sparire il codice, lo spostano in posti più appropriati. Qual è il tuo problema con quello? "ma i miei complimenti, almeno 5 utenti hanno preso l'esca" Sembra molto più stronzo di quanto io pensi (spero) tu abbia voluto.
Alexander - Ripristina Monica il

@Falco Mi sembra un nuovo metodo (in questa classe di calcolatrice di pagamenti, non DateInterval):calculatePayPeriod(startData, totalPayment, monthlyPayment)
Alexander - Reinstalla Monica il

7

A volte le espressioni fluide aiutano su questo:

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

Dato il tempo sufficiente per progettare, puoi creare un'API solida che agisce in modo simile a un linguaggio specifico del dominio.

L'altro grande vantaggio è che gli IDE con il completamento automatico rendono quasi irrilevante la lettura della documentazione dell'API, poiché è intuitivo a causa delle sue capacità auto-individuabili.

Ci sono risorse là fuori come https://nikas.praninskas.com/javascript/2015/04/26/fluent-javascript/ o https://github.com/nikaspran/fluent.js su questo argomento.

Esempio (tratto dal primo collegamento di risorse):

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]

8
Un'interfaccia fluida da sola non rende alcun compito particolare più facile o più difficile. Questo sembra più simile al modello Builder.
VLAZ,

8
L'implementazione sarebbe piuttosto complicata, tuttavia, se è necessario prevenire chiamate errate comeforTotalAmount(1234).breakIntoPayments().byPeriod(2).monthly().withPaymentsOf(12.34).byDateRange(saleStart, saleEnd);
Bergi,

4
Se gli sviluppatori vogliono davvero sparare in piedi, ci sono modi più semplici @Bergi. Tuttavia, l'esempio che metti è molto più leggibile diforTotalAmountAndBreakIntoPaymentsByPeriodThenMonthlyWithPaymentsOfButByDateRange(1234, 2, 12.34, saleStart, saleEnd);
DanielCuadra,

5
@DanielCuadra Il punto che stavo cercando di sottolineare è che la tua risposta non risolve davvero il problema dei PO di avere 3 parametri reciprocamente esclusivi. L'uso del modello builder potrebbe rendere la chiamata più leggibile (e aumentare la probabilità che l'utente si accorga che non ha senso), ma l'utilizzo del modello builder da solo non impedisce loro di passare ancora 3 valori contemporaneamente.
Bergi,

2
@Falco Will? Sì, è possibile, ma più complicato, e la risposta non ha fatto menzione di questo. I costruttori più comuni che ho visto consistevano in una sola classe. Se la risposta viene modificata in modo da includere il codice del / dei costruttore / i, sarò felice di approvarlo e rimuovere il mio voto negativo.
Bergi,

2

Bene, in altre lingue, useresti parametri denominati . Questo può essere emulato in Javscript:

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

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

6
Come il modello di generatore di seguito, questo rende la chiamata più leggibile (e aumenta la probabilità che l'utente si accorga che non ha senso), ma nominare i parametri non impedisce all'utente di passare ancora 3 valori contemporaneamente - ad es getPaymentBreakdown(100, today, {endDate: whatever, noOfMonths: 4, monthlyPayment: 20}).
Bergi,

1
Non dovrebbe essere :invece di =?
Barmar,

Suppongo che potresti verificare che solo uno dei parametri sia non nullo (o non sia presente nel dizionario).
Mateen Ulhaq,

1
@Bergi - La stessa sintassi non impedisce agli utenti di passare parametri senza senso, ma puoi semplicemente fare un po 'di validazione e generare errori
slebetman

@Bergi Non sono affatto un esperto di Javascript, ma penso che l'assegnazione di destrutturazione in ES6 possa essere d'aiuto, anche se sono molto leggero sulla conoscenza di questo.
Gregory Currie,

1

In alternativa potresti anche rompere la responsabilità di specificare il numero del mese e lasciarlo fuori dalla tua funzione:

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

E getpaymentBreakdown riceverà un oggetto che fornirà il numero base di mesi

Quelli avrebbero una funzione di ordine superiore restituendo ad esempio una funzione.

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

Che cosa è successo ai parametri totale startDate?
Bergi,

Sembra una bella API, ma potresti aggiungere come immagineresti di implementare queste quattro funzioni? (Con i tipi di variante e un'interfaccia comune questo potrebbe essere abbastanza elegante, ma non è chiaro cosa avevi in ​​mente).
Bergi,

@Bergi ha modificato il mio post
Vinz243,

0

E se dovessi lavorare con un sistema con sindacati / tipi di dati algebrici discriminati, potresti passare come, per esempio, a TimePeriodSpecification.

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

e quindi nessuno dei problemi si verificherebbe dove potresti non riuscire a implementarne effettivamente uno e così via.


Questo è sicuramente il modo in cui lo affronterei in una lingua che aveva tipi come questi disponibili. Ho cercato di mantenere la mia domanda agnostica sul linguaggio, ma forse dovrebbe tener conto del linguaggio usato perché in alcuni casi sono possibili approcci come questo.
CalMlynarczyk,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.