Tenemos una función API que desglosa un monto total en montos mensuales en función de las fechas de inicio y finalización.
// 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,
};
}
Recientemente, un consumidor de esta API quería especificar el rango de fechas de otras maneras: 1) proporcionando el número de meses en lugar de la fecha de finalización, o 2) proporcionando el pago mensual y calculando la fecha de finalización. En respuesta a esto, el equipo de API cambió la función a la siguiente:
// 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),
};
}
Siento que este cambio complica la API. Ahora la persona que llama tiene que preocuparse por la heurística ocultos en la implementación de la función en la determinación de los parámetros que tienen preferencia en la que se utiliza para calcular el intervalo de fechas (es decir, por orden de prioridad monthlyPayment
, numMonths
, endDate
). Si una persona que llama no presta atención a la firma de la función, puede enviar varios de los parámetros opcionales y confundirse sobre por qué endDate
se ignora. Sí especificamos este comportamiento en la documentación de la función.
Además, creo que sienta un mal precedente y agrega responsabilidades a la API con las que no debería preocuparse (es decir, violar SRP). Suponga que consumidores adicionales desean que la función admita más casos de uso, como el cálculo a total
partir de los parámetros numMonths
y monthlyPayment
. Esta función se volverá cada vez más complicada con el tiempo.
Mi preferencia es mantener la función como estaba y, en cambio, exigirle a la persona que llama que se calcule endDate
. Sin embargo, puedo estar equivocado y me preguntaba si los cambios que hicieron fueron una forma aceptable de diseñar una función API.
Alternativamente, ¿hay un patrón común para manejar escenarios como este? Podríamos proporcionar funciones adicionales de orden superior en nuestra API que envuelvan la función original, pero esto aumenta la API. Tal vez podríamos agregar un parámetro de indicador adicional que especifique qué enfoque usar dentro de la función.
fuente
Date
- se puede suministrar una cadena y que se pueden analizar para determinar la fecha. Sin embargo, de esta manera, los parámetros de manejo también pueden ser muy delicados y pueden producir resultados poco confiables. Ver deDate
nuevo. No es imposible hacerlo bien: Moment lo maneja mucho mejor, pero es muy molesto de usar de todos modos.monthlyPayment
se da perototal
no es un múltiplo entero. Y también cómo lidiar con posibles errores de redondeo de punto flotante si no se garantiza que los valores sean enteros (por ejemplo, pruébelo contotal = 0.3
ymonthlyPayment = 0.1
).Respuestas:
Al ver la implementación, me parece que lo que realmente necesita aquí son 3 funciones diferentes en lugar de una:
El original:
El que proporciona el número de meses en lugar de la fecha de finalización:
y el que proporciona el pago mensual y calcula la fecha de finalización:
Ahora, ya no hay parámetros opcionales, y debería quedar bastante claro qué función se llama cómo y para qué propósito. Como se menciona en los comentarios, en un lenguaje estrictamente tipado, uno también podría utilizar la sobrecarga de funciones, distinguiendo las 3 funciones diferentes no necesariamente por su nombre, sino por su firma, en caso de que esto no ofusque su propósito.
Tenga en cuenta que las diferentes funciones no significan que tenga que duplicar ninguna lógica: internamente, si estas funciones comparten un algoritmo común, se debe refactorizar a una función "privada".
No creo que haya un "patrón" (en el sentido de los patrones de diseño de GoF) que describa un buen diseño de API. El uso de nombres autodescriptivos, funciones con menos parámetros, funciones con parámetros ortogonales (= independientes), son solo principios básicos para crear código legible, mantenible y evolutivo. No toda buena idea en programación es necesariamente un "patrón de diseño".
fuente
getPaymentBreakdown
(o realmente cualquiera de esos 3) y las otras dos funciones simplemente convierten los argumentos y llaman a eso. ¿Por qué agregar una función privada que sea una copia perfecta de uno de estos 3?innerNumMonths
,total
ystartDate
. ¿Por qué mantener una función demasiado complicada con 5 parámetros, donde 3 son casi opcionales (excepto que se debe configurar uno), cuando una función de 3 parámetros también hará el trabajo?getPaymentBreakdown(total, startDate, endDate)
función pública como implementación común, la otra herramienta simplemente calculará las fechas totales / de inicio / finalización adecuadas y lo llamará.getPaymentBreakdown
la pregunta.Estás exactamente en lo correcto.
Esto tampoco es ideal, porque el código de la persona que llama estará contaminado con una placa de caldera no relacionada.
Introducir un nuevo tipo, como
DateInterval
. Agregue los constructores que tengan sentido (fecha de inicio + fecha de finalización, fecha de inicio + número de meses, lo que sea). Adopte esto como los tipos de moneda común para expresar intervalos de fechas / horas en todo su sistema.fuente
DateInterval
):calculatePayPeriod(startData, totalPayment, monthlyPayment)
A veces, las expresiones fluidas ayudan en esto:
Con tiempo suficiente para diseñar, puede llegar a una API sólida que actúe de manera similar a un lenguaje específico de dominio.
La otra gran ventaja es que los IDE con autocompletado hacen que sea casi irreverente leer la documentación de la API, como es intuitivo debido a sus capacidades de autodescubrimiento.
Existen recursos como https://nikas.praninskas.com/javascript/2015/04/26/fluent-javascript/ o https://github.com/nikaspran/fluent.js sobre este tema.
Ejemplo (tomado del primer enlace de recursos):
fuente
forTotalAmount(1234).breakIntoPayments().byPeriod(2).monthly().withPaymentsOf(12.34).byDateRange(saleStart, saleEnd);
forTotalAmountAndBreakIntoPaymentsByPeriodThenMonthlyWithPaymentsOfButByDateRange(1234, 2, 12.34, saleStart, saleEnd);
Bueno, en otros idiomas, usarías parámetros con nombre . Esto se puede emular en Javscript:
fuente
getPaymentBreakdown(100, today, {endDate: whatever, noOfMonths: 4, monthlyPayment: 20})
.:
lugar de=
?Como alternativa, también puede romper la responsabilidad de especificar el número de meses y dejarlo fuera de su función:
Y getpaymentBreakdown recibiría un objeto que proporcionaría el número base de meses
Esas funciones de orden superior devolverían, por ejemplo, una función.
fuente
total
ystartDate
?Y si estuviera trabajando con un sistema con uniones discriminadas / tipos de datos algebraicos, podría pasarlo como, por ejemplo, a
TimePeriodSpecification
.y luego ninguno de los problemas ocurriría cuando no pudieras implementar uno y así sucesivamente.
fuente