Чи існує схема обробки конфліктуючих параметрів функції?


38

У нас є функція API, яка розбиває загальну суму на щомісячні суми залежно від дати початку та закінчення.

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

Нещодавно споживач цього API хотів вказати діапазон дат іншими способами: 1) шляхом надання кількості місяців замість дати закінчення, або 2) шляхом надання щомісячного платежу та підрахунку дати закінчення. У відповідь на це команда API змінила функцію на наступну:

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

Я думаю, що ця зміна ускладнює API. Тепер абонент повинен турбуватися про евристиках захованих з реалізацією функції у визначенні того, які параметри приймають перевагу в даний час використовується для розрахунку діапазону дат (тобто в порядку пріоритету monthlyPayment, numMonths, endDate). Якщо абонент не звертає уваги на підпис функції, він може надіслати кілька необов'язкових параметрів і заплутатися в тому, чому endDateйого ігнорують. Ми вказуємо цю поведінку в документації щодо функцій.

Крім того, я вважаю, що це створює поганий прецедент і додає API відповідальності, з якою він не повинен стосуватися себе (тобто порушує SRP). Нехай додаткові споживачі хочуть функції для підтримки більшого числа випадків використання, наприклад, обчислення totalвід numMonthsі monthlyPaymentпараметрів. Ця функція з часом буде ускладнюватися.

Моя перевага - зберегти функцію такою, якою вона була, і замість цього вимагати від абонента розрахувати endDateсебе. Однак я можу помилитися і мені було цікаво, чи внесені ними зміни є прийнятним способом розробити функцію API.

Як варіант, чи існує загальна модель для обробки подібних сценаріїв? Ми могли б надати додаткові функції вищого порядку в нашому API, які містять оригінальну функцію, але це роздуває API. Можливо, ми могли б додати додатковий параметр прапора, який визначає, який підхід використовувати всередині функції.


79
"Нещодавно споживач цього API хотів [надати] кількість місяців замість дати закінчення" - Це легковажний запит. Вони можуть перетворити # місяця на належну дату закінчення у рядку або коду на їх кінці.
Грем

12
це схоже на анти-візерунок "Аргумент
прапорця"

2
Як бічна примітка, є функції, які можуть приймати один і той же тип і кількість параметрів і давати дуже різні результати на основі таких - див. Date- Ви можете надати рядок, і це можна проаналізувати, щоб визначити дату. Однак такий спосіб керування параметрами може також бути дуже вибагливим і може призвести до ненадійних результатів. Побачимо Dateще раз. Зробити це правильно неможливо - Момент справляється з цим набагато краще, але користуватися ним дуже прикро.
ВЛАЗ

По незначній дотичній, можливо, ви захочете подумати про те, як розібратися із випадком, коли monthlyPaymentвін вказаний, але totalце не ціле число, кратне його. А також як боротися з можливими помилками округлення плаваючої точки, якщо значення не гарантуються цілими числами (наприклад, спробуйте це з total = 0.3і monthlyPayment = 0.1).
Ільмарі Каронен

@Graham Я на це не відреагував ... Я відреагував на наступне твердження "У відповідь на це команда API змінила функцію ..." - перекидається в положення плода і починає гойдатися - Не має значення, де цей рядок або два коду йде, або новий виклик API з іншим форматом, або виконаний на кінці виклику. Просто не змінюйте робочий виклик API, як це!
Балдрікк

Відповіді:


99

Бачачи реалізацію, мені здається, що вам справді потрібні ось три різні функції замість однієї:

Оригінальний:

function getPaymentBreakdown(total, startDate, endDate) 

Той, що містить кількість місяців замість дати закінчення:

function getPaymentBreakdownByNoOfMonths(total, startDate, noOfMonths) 

і той, що забезпечує щомісячний платіж та обчислює дату закінчення:

function getPaymentBreakdownByMonthlyPayment(total, startDate, monthlyPayment) 

Тепер вже немає додаткових параметрів, і повинно бути досить зрозуміло, яка функція називається як і з якою метою. Як зазначалося в коментарях, строго набраною мовою можна було також використовувати функцію перевантаження, виділяючи 3 різні функції не обов'язково за своїм ім'ям, а за їх підписом, якщо це не заплутує їх призначення.

Зауважте, що різні функції не означають, що вам потрібно дублювати будь-яку логіку - якщо всередині цих функцій спільний алгоритм, він повинен бути відновлений до "приватної" функції.

чи існує загальна модель для обробки подібних сценаріїв

Я не думаю, що існує "модель" (в сенсі моделей дизайну GoF), яка описує хороший дизайн API. Використання самостійно описуючих імен, функцій із меншою кількістю параметрів, функцій з ортогональними (= незалежними) параметрами - лише основні принципи створення читабельного, підтримуваного та еволюційного коду. Не кожна гарна ідея в програмуванні обов'язково є «дизайнерською схемою».


24
Насправді "загальною" реалізацією коду може бути просто getPaymentBreakdown(або насправді будь-яка з цих 3), а інші дві функції просто перетворюють аргументи і викликають це. Навіщо додати приватну функцію, яка є ідеальною копією одного з цих 3?
Джакомо Альзетта,

@GiacomoAlzetta: це можливо. Але я впевнений , що реалізація стане простіше, надаючи загальну функцію , яка містить тільки «повернення» частина функції OPS, і нехай громадськість 3 функції викликати цю функцію з параметрами innerNumMonths, totalі startDate. Навіщо зберігати надскладну функцію з 5 параметрами, де 3 є майже необов’язковими (крім одного потрібно встановити), коли 3-параметрична функція також виконає цю роботу?
Doc Brown

3
Я не хотів сказати "збережіть функцію 5 аргументів". Я просто кажу, що коли у вас є загальна логіка, ця логіка не повинна бути приватною . У цьому випадку всі 3 функції можна відновити, щоб просто перетворити параметри на дати початку та закінчення, тому ви можете використовувати загальнодоступну getPaymentBreakdown(total, startDate, endDate)функцію як загальну реалізацію, інший інструмент просто обчислить відповідні загальні / початкові / кінцеві дати та назве її.
Джакомо Альзетта

@GiacomoAlzetta: гаразд, було непорозуміння, я вважав, що ви говорите про другу реалізацію getPaymentBreakdownцього питання.
Doc Brown

Я б пішов до того, щоб додати нову версію оригінального методу, яка явно називається "getPaymentBreakdownByStartAndEnd", і знецінити оригінальний метод, якщо ви хочете надати все це.
Ерік

20

Крім того, я вважаю, що це створює поганий прецедент і додає API відповідальності, з якою він не повинен стосуватися себе (тобто порушує SRP). Нехай додаткові споживачі хочуть функції для підтримки більшого числа випадків використання, наприклад, обчислення totalвід numMonthsі monthlyPaymentпараметрів. Ця функція з часом буде ускладнюватися.

Ви абсолютно правильні.

Моя перевага полягає в тому, щоб зберегти функцію такою, якою вона була, і замість цього вимагати, щоб абонент сам розрахував endDate. Однак я можу помилитися і мені було цікаво, чи внесені ними зміни є прийнятним способом розробити функцію API.

Це також не ідеально, оскільки код абонента буде забруднений незв'язаною плитою котла.

Як варіант, чи існує загальна модель для обробки подібних сценаріїв?

Введіть новий тип, як DateInterval. Додайте сенси конструкторів (дата початку + кінцева дата, дата початку + число місяців тощо). Прийміть це як типи загальної валюти для вираження інтервалів дат / разів у всій вашій системі.


3
@DocBrown Yep. У таких випадках (Ruby, Python, JS) прийнято просто використовувати статичні / класичні методи. Але це детальна інформація про реалізацію, яка, на мою думку, не є особливо актуальною для моєї відповіді ("використовувати тип").
Олександр

2
І ця ідея , до жаль , сягає свої межі з третьою вимогою: Датою початку, загальним платежем і щомісячним платежем - і функція буде обчислювати DateInterval від параметрів грошей - і ви не повинні покласти грошові суми в свій діапазон дат ...
Falco

3
@DocBrown "перекладає проблему лише з існуючої функції на конструктор типу" Так, він ставить тимчасовий код, куди повинен пройти часовий код, щоб грошовий код міг бути там, де повинен піти грошовий код. Це просто SRP, тому я не впевнений, що ти отримуєш, коли ти кажеш, що це "лише" зміщує проблему. Ось що роблять усі функції. Вони не змушують код відходити, вони переміщують його у більш відповідні місця. У чому полягає ваша проблема з цим? "але мої привітання, принаймні 5 учасників прикормки взяли приманку" Це звучить набагато дурніше, ніж я думаю (сподіваюся), що ви задумали.
Олександр

@Falco Це звучить як новий метод для мене (на цьому класі калькулятор платежів, ні DateInterval):calculatePayPeriod(startData, totalPayment, monthlyPayment)
Олександр,

7

Іноді в цьому допомагають вільні вирази:

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

Виділивши достатньо часу для розробки, ви можете створити надійний API, який діє аналогічно мові, що залежить від домену.

Інша велика перевага полягає в тому, що IDE з автозаповненням змушують майже не зважати на читання документації API, як це інтуїтивно зрозуміло завдяки можливостям самовідкриття.

Там є такі ресурси, як https://nikas.praninskas.com/javascript/2015/04/26/fluent-javascript/ або https://github.com/nikaspran/fluent.js з цієї теми.

Приклад (взятий із першого посилання на ресурс):

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
Вільний інтерфейс сам по собі не робить якусь конкретну задачу простішою чи складнішою. Це більше схоже на модель Builder.
ВЛАЗ

8
Реалізація була б досить складною, хоча вам потрібно запобігти помилковим дзвінкам, наприкладforTotalAmount(1234).breakIntoPayments().byPeriod(2).monthly().withPaymentsOf(12.34).byDateRange(saleStart, saleEnd);
Бергі,

4
Якщо розробники справді хочуть стріляти по ногах, існують простіші способи @Bergi. І все-таки приклад, який ви forTotalAmountAndBreakIntoPaymentsByPeriodThenMonthlyWithPaymentsOfButByDateRange(1234, 2, 12.34, saleStart, saleEnd);
подали,

5
@DanielCuadra Точка, яку я намагався зробити, - це те, що ваша відповідь насправді не вирішує проблему ОП щодо наявності 3 взаємовиключних параметрів. Використання схеми конструктора може зробити виклик більш читабельним (і підвищити ймовірність того, що користувач помітить, що це не має сенсу), але використання самого шаблона побудови не заважає їм все-таки передавати 3 значення відразу.
Бергі

2
@Falco Буде? Так, це можливо, але складніше, і відповідь про це не згадував. Більш поширені будівельники, яких я бачив, складалися лише з одного класу. Якщо відповідь буде відредагована, щоб вона включала код будівельника, я з радістю схвалюю її та видаляю свою підсумкову заявку.
Бергі

2

Ну, в інших мовах ви б використовували названі параметри . Це можна наслідувати у Javscript:

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

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

6
Як і схема, що складається нижче, це робить виклик більш читабельним (і збільшує ймовірність того, що користувач помітить, що це не має сенсу), але іменування параметрів не заважає користувачеві все одно передавати 3 значення одразу - наприклад getPaymentBreakdown(100, today, {endDate: whatever, noOfMonths: 4, monthlyPayment: 20}).
Бергі

1
Чи не повинно бути :замість цього =?
Бармар

Напевно, ви могли перевірити, що лише один з параметрів є ненульовим (або його немає у словнику).
Mateen Ulhaq

1
@Bergi - Сам синтаксис не заважає користувачам приймати безглузді параметри, але ви можете просто зробити перевірку та викинути помилки
slebetman

@Bergi Я аж ніяк не експерт Javascript, але я вважаю, що присвоєння деструктуризації в ES6 може допомогти тут, хоча я дуже добре знаюсь з цим.
Григорій Керрі

1

Як альтернативу, ви також можете зламати відповідальність за вказівку кількості місяця та виключити її зі своєї функції:

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

І getpaymentBreakdown отримає об'єкт, який забезпечить базову кількість місяців

Це буде функція вищого порядку, повертаючи, наприклад, функцію.

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

Що сталося з параметрами totalі startDate?
Бергі

Це здається приємним API, але ви можете, будь ласка, додати, як би ви уявили ці чотири функції, які потрібно реалізувати? (З типами варіантів та загальним інтерфейсом це може бути досить елегантно, але незрозуміло, що ви мали на увазі).
Бергі

@Bergi відредагував моє повідомлення
Vinz243

0

І якби ви працювали із системою з дискримінаційними типами даних союзів / алгебраїчних даних, ви могли б передати її як, скажімо, a TimePeriodSpecification.

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

і тоді жодна з проблем не виникне там, де ви не зможете реально реалізувати її тощо.


Це, безумовно, як я підходив би до цього мовою, на якій були доступні такі типи. Я намагався зберегти своє питання мова-агностик, але, можливо, він повинен враховувати мову, що використовується, оскільки такі підходи в деяких випадках можливі.
CalMlynarczyk
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.