Mamy funkcję API, która dzieli całkowitą kwotę na kwoty miesięczne na podstawie danych dat rozpoczęcia i zakończenia.
// 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,
};
}
Ostatnio konsument tego interfejsu API chciał określić zakres dat na inne sposoby: 1) podając liczbę miesięcy zamiast daty końcowej, lub 2) podając miesięczną płatność i obliczając datę końcową. W odpowiedzi zespół API zmienił funkcję na:
// 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),
};
}
Wydaje mi się, że ta zmiana komplikuje interfejs API. Teraz rozmówca musi się martwić o ukryte heurystyki z realizacją danej funkcji w celu ustalenia, które parametry mają pierwszeństwo w wykorzystywane do obliczenia zakres dat (czyli w kolejności priorytetów monthlyPayment
, numMonths
, endDate
). Jeśli osoba dzwoniąca nie zwraca uwagi na podpis funkcji, może wysłać wiele opcjonalnych parametrów i pomylić się, dlaczego endDate
jest ignorowana. Określamy to zachowanie w dokumentacji funkcji.
Dodatkowo uważam, że stanowi zły precedens i dodaje obowiązki do API, którym nie powinien się zajmować (tj. Naruszać SRP). Załóżmy, że dodatkowi klienci chcą, aby funkcja obsługiwała więcej przypadków użycia, takich jak obliczanie total
na podstawie parametrów numMonths
i monthlyPayment
. Ta funkcja z czasem będzie się komplikować.
Preferuję, aby funkcja pozostała taka, jaka była, a zamiast tego wymagać od osoby dzwoniącej obliczenia endDate
siebie. Jednak mogę się mylić i zastanawiałem się, czy wprowadzone przez nich zmiany były akceptowalnym sposobem zaprojektowania funkcji API.
Alternatywnie, czy istnieje wspólny wzorzec obsługi takich scenariuszy? Możemy zapewnić dodatkowe funkcje wyższego rzędu w naszym interfejsie API, które zawijają oryginalną funkcję, ale powoduje to powiększenie interfejsu API. Być może moglibyśmy dodać dodatkowy parametr flagi określający, które podejście należy zastosować w funkcji.
źródło
Date
- można dostarczyć łańcuch i może być analizowany w celu ustalenia terminu. Jednak w ten sposób parametry obsługi mogą być również bardzo wybredne i mogą dawać niewiarygodne wyniki. ZobaczDate
jeszcze raz. Właściwie nie jest to niemożliwe - Moment radzi sobie z tym lepiej, ale korzystanie z niego jest bardzo denerwujące.monthlyPayment
jest podany, aletotal
nie jest jego całkowitą wielokrotnością. A także, jak radzić sobie z możliwymi błędami zaokrąglania zmiennoprzecinkowego, jeśli nie można zagwarantować, że wartości są liczbami całkowitymi (np. Spróbuj za pomocątotal = 0.3
imonthlyPayment = 0.1
).Odpowiedzi:
Widząc implementację, wydaje mi się, że tak naprawdę potrzebujesz tutaj 3 różnych funkcji zamiast jednej:
Oryginalny:
Ten, który podaje liczbę miesięcy zamiast daty końcowej:
i ten zapewniający miesięczną płatność i obliczający datę końcową:
Teraz nie ma już żadnych parametrów opcjonalnych i powinno być całkiem jasne, która funkcja nazywa się w jaki sposób i w jakim celu. Jak wspomniano w komentarzach, w ściśle typowanym języku można również wykorzystać przeciążenie funkcji, rozróżniając 3 różne funkcje niekoniecznie według nazwy, ale według ich podpisu, w przypadku, gdy nie zaciemnia to ich celu.
Zauważ, że różne funkcje nie oznaczają, że musisz zduplikować dowolną logikę - wewnętrznie, jeśli funkcje te mają wspólny algorytm, należy przekształcić je w funkcję „prywatną”.
Nie sądzę, że istnieje „wzorzec” (w sensie wzorców projektowych GoF), który opisuje dobry projekt API. Używanie samoopisujących nazw, funkcje z mniejszą liczbą parametrów, funkcje z ortogonalnymi (= niezależnymi) parametrami, to tylko podstawowe zasady tworzenia czytelnego, łatwego do utrzymania i rozwijalnego kodu. Nie każdy dobry pomysł w programowaniu jest koniecznie „wzorcem projektowym”.
źródło
getPaymentBreakdown
(a właściwie dowolna z tych 3), a pozostałe dwie funkcje po prostu konwertują argumenty i wywołują to. Po co dodawać prywatną funkcję, która jest idealną kopią jednego z tych 3?innerNumMonths
,total
astartDate
. Po co utrzymywać nadmiernie skomplikowaną funkcję z 5 parametrami, gdzie 3 są prawie opcjonalne (z wyjątkiem tego, że jeden musi być ustawiony), gdy funkcja 3-parametrowa również wykona zadanie?getPaymentBreakdown(total, startDate, endDate)
funkcji publicznej jako wspólnej implementacji, drugie narzędzie po prostu obliczy odpowiednią całkowitą / początkową / końcową datę i ją wywoła.getPaymentBreakdown
pytania.Masz dokładnie rację.
Nie jest to również idealne, ponieważ kod wywołujący zostanie zanieczyszczony niepowiązaną płytą kotła.
Wprowadź nowy typ, np
DateInterval
. Dodaj dowolne konstruktory, które mają sens (data początkowa + data końcowa, data początkowa + liczba miesięcy, cokolwiek.) Zaadoptuj to jako typ wspólnej waluty do wyrażania przedziałów dat / godzin w całym systemie.źródło
DateInterval
):calculatePayPeriod(startData, totalPayment, monthlyPayment)
Czasami pomagają w tym płynne wyrażenia:
Mając wystarczająco dużo czasu na zaprojektowanie, możesz opracować solidny interfejs API, który działa podobnie do języka specyficznego dla domeny.
Inną dużą zaletą jest to, że środowiska IDE z funkcją autouzupełniania sprawiają, że czytanie dokumentacji API jest prawie nierealne, ponieważ jest intuicyjne ze względu na możliwości samodzielnego odkrywania.
Istnieją zasoby, takie jak https://nikas.praninskas.com/javascript/2015/04/26/fluent-javascript/ lub https://github.com/nikaspran/fluent.js na ten temat.
Przykład (wzięty z pierwszego łącza do zasobów):
źródło
forTotalAmount(1234).breakIntoPayments().byPeriod(2).monthly().withPaymentsOf(12.34).byDateRange(saleStart, saleEnd);
forTotalAmountAndBreakIntoPaymentsByPeriodThenMonthlyWithPaymentsOfButByDateRange(1234, 2, 12.34, saleStart, saleEnd);
W innych językach używasz nazwanych parametrów . Można to emulować w Javscript:
źródło
getPaymentBreakdown(100, today, {endDate: whatever, noOfMonths: 4, monthlyPayment: 20})
.:
zamiast=
?Alternatywnie możesz również zerwać odpowiedzialność za określenie liczby miesięcy i pominąć tę funkcję:
A getpaymentBreakdown otrzyma obiekt, który zapewni podstawową liczbę miesięcy
Byłyby to funkcje wyższego rzędu zwracające na przykład funkcję.
źródło
total
istartDate
?A jeśli miałbyś pracować z systemem z dyskryminowanymi związkami / algebraicznymi typami danych, możesz przekazać to jak, powiedzmy, a
TimePeriodSpecification
.i wtedy nie wystąpiłby żaden problem, w którym można nie wdrożyć jednego i tak dalej.
źródło