Temos uma função de API que divide um valor total em valores mensais com base nas datas de início e término.
// 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,
};
}
Recentemente, um consumidor dessa API desejou especificar o período de outras maneiras: 1) fornecendo o número de meses em vez da data final ou 2) fornecendo o pagamento mensal e calculando a data final. Em resposta a isso, a equipe da API alterou a função para o seguinte:
// 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),
};
}
Eu sinto que essa mudança complica a API. Agora, o chamador precisa se preocupar com a heurística escondidas com a implementação da função na determinação de quais parâmetros têm preferência em ser usado para calcular o intervalo de datas (ou seja, por ordem de prioridade monthlyPayment
, numMonths
, endDate
). Se um chamador não prestar atenção na assinatura da função, ele poderá enviar vários parâmetros opcionais e ficar confuso sobre o motivo pelo qual endDate
está sendo ignorado. Nós especificamos esse comportamento na documentação da função.
Além disso, acho que ele cria um precedente ruim e adiciona responsabilidades à API com as quais ela não deve se preocupar (ou seja, violar o SRP). Suponha que consumidores adicionais desejem que a função suporte mais casos de uso, como o cálculo total
dos parâmetros numMonths
e monthlyPayment
. Esta função se tornará cada vez mais complicada ao longo do tempo.
Minha preferência é manter a função como era e exigir que o chamador se calcule endDate
. No entanto, posso estar errado e queria saber se as alterações feitas foram uma maneira aceitável de criar uma função de API.
Como alternativa, existe um padrão comum para lidar com cenários como este? Poderíamos fornecer funções adicionais de ordem superior em nossa API que envolvam a função original, mas isso incha a API. Talvez possamos adicionar um parâmetro de flag adicional especificando qual abordagem usar dentro da função.
fonte
Date
- você pode fornecer uma corda e ele pode ser analisado para determinar a data. No entanto, desta forma, oh parâmetros de manipulação também podem ser muito exigentes e podem produzir resultados não confiáveis. VejaDate
novamente. Não é impossível fazer o certo - o Moment lida com isso muito melhor, mas é muito chato de usar, independentemente.monthlyPayment
é dado, mastotal
não é um múltiplo inteiro dele. E também como lidar com possíveis erros de arredondamento de ponto flutuante se não for garantido que os valores sejam inteiros (por exemplo, tente comtotal = 0.3
emonthlyPayment = 0.1
).Respostas:
Vendo a implementação, parece-me o que você realmente precisa aqui são três funções diferentes em vez de uma:
O original:
Aquele que fornece o número de meses em vez da data final:
e aquele que fornece o pagamento mensal e calcula a data final:
Agora, não há mais parâmetros opcionais, e deve ficar bem claro qual função é chamada como e para qual finalidade. Como mencionado nos comentários, em uma linguagem estritamente digitada, também é possível utilizar a sobrecarga de funções, distinguindo as três funções diferentes, não necessariamente pelo nome, mas pela assinatura, caso isso não oculte seu objetivo.
Observe que as diferentes funções não significam que você precise duplicar qualquer lógica - internamente, se essas funções compartilham um algoritmo comum, ele deve ser refatorado para uma função "privada".
Eu não acho que exista um "padrão" (no sentido dos padrões de design do GoF) que descreve um bom design de API. O uso de nomes autoexplicativos, funções com menos parâmetros, funções com parâmetros ortogonais (= independentes), são apenas princípios básicos da criação de código legível, sustentável e evolutível. Nem toda boa ideia de programação é necessariamente um "padrão de design".
fonte
getPaymentBreakdown
(ou realmente qualquer uma dessas 3) e as outras duas funções apenas convertem os argumentos e chamam isso. Por que adicionar uma função privada que é uma cópia perfeita de um desses 3?innerNumMonths
,total
estartDate
. Por que manter uma função complicada demais com 5 parâmetros, onde 3 são quase opcionais (exceto que um precisa ser definido), quando uma função de 3 parâmetros também faz o trabalho?getPaymentBreakdown(total, startDate, endDate)
função pública como implementação comum, a outra ferramenta simplesmente calculará as datas totais / de início / fim adequadas e a chamará.getPaymentBreakdown
questão.Você está exatamente correto.
Isso também não é ideal, porque o código do chamador será poluído com uma placa de caldeira não relacionada.
Introduzir um novo tipo, como
DateInterval
. Adicione o que os construtores fizerem sentido (data de início + data de término, data de início + num meses, qualquer que seja.). Adote isso como os tipos de moeda comum para expressar intervalos de datas / horas em todo o sistema.fonte
DateInterval
):calculatePayPeriod(startData, totalPayment, monthlyPayment)
Às vezes, expressões fluentes ajudam nisso:
Com tempo suficiente para projetar, você pode criar uma API sólida que age de maneira semelhante a uma linguagem específica de domínio.
A outra grande vantagem é que os IDEs com preenchimento automático tornam quase irrelevante a leitura da documentação da API, pois são intuitivos devido a seus recursos auto-detectáveis.
Existem recursos por aí, como https://nikas.praninskas.com/javascript/2015/04/26/fluent-javascript/ ou https://github.com/nikaspran/fluent.js sobre este tópico.
Exemplo (retirado do primeiro link do recurso):
fonte
forTotalAmount(1234).breakIntoPayments().byPeriod(2).monthly().withPaymentsOf(12.34).byDateRange(saleStart, saleEnd);
forTotalAmountAndBreakIntoPaymentsByPeriodThenMonthlyWithPaymentsOfButByDateRange(1234, 2, 12.34, saleStart, saleEnd);
Bem, em outros idiomas, você usaria parâmetros nomeados . Isso pode ser emulado no Javscript:
fonte
getPaymentBreakdown(100, today, {endDate: whatever, noOfMonths: 4, monthlyPayment: 20})
.:
vez de=
?Como alternativa, você também pode quebrar a responsabilidade de especificar o número do mês e deixá-lo fora de sua função:
E o getpaymentBreakdown receberia um objeto que forneceria o número base de meses
Essas funções de ordem superior retornariam, por exemplo, uma função.
fonte
total
estartDate
?E se você estivesse trabalhando com um sistema com uniões discriminadas / tipos de dados algébricos, poderia repassá-lo como, digamos, a
TimePeriodSpecification
.e então nenhum dos problemas ocorreria onde você poderia falhar na implementação de um e assim por diante.
fonte