충돌하는 기능 매개 변수를 처리하기위한 패턴이 있습니까?


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 위반). 추가 소비자 totalnumMonthsmonthlyPayment매개 변수 계산과 같은 더 많은 사용 사례를 지원하는 함수를 원한다고 가정 합니다. 이 기능은 시간이 지남에 따라 점점 더 복잡해질 것입니다.

선호하는 것은 함수를 그대로 유지하고 대신 호출자가 endDate스스로 계산 하도록 요구하는 것 입니다. 그러나, 내가 틀렸을 수도 있고 변경 사항이 API 기능을 설계하는 데 적합한 방법인지 궁금합니다.

또는 이와 같은 시나리오를 처리하기위한 일반적인 패턴이 있습니까? 우리는 원래 함수를 감싸는 추가 고차 함수를 API에 제공 할 수 있지만, 이는 API를 팽창시킵니다. 함수 내부에서 사용할 접근법을 지정하는 추가 플래그 매개 변수를 추가 할 수 있습니다.


79
"최근에,이 API의 소비자는 종료 날짜 대신 개월 수를 제공하려고했습니다."-이것은 매우 까다로운 요청입니다. 월 수를 코드 한 줄 또는 두 줄로 적절한 종료 날짜로 변환 할 수 있습니다.
Graham

12
플래그 인수 안티 패턴처럼 보이는, 그리고 나 또한 몇 가지 기능으로 분할하는 것이 좋습니다 것이라고
njzk2

2
참고로, 동일한 유형과 개수의 매개 변수를 허용하고 그에 따라 매우 다른 결과를 생성 할 수 있는 함수가 있습니다 Date. 그러나 이런 식으로 매개 변수를 처리하는 것도 매우 까다 롭고 신뢰할 수없는 결과를 생성 할 수 있습니다. Date다시 참조 하십시오. 올바른 일을하는 것이 불가능하지는 않습니다.-Moment가 더 잘 처리하지만 관계없이 사용하는 것은 매우 성가신 일입니다.
VLAZ

약간의 접선에서는 monthlyPayment주어진 값이지만 total정수의 배수가 아닌 경우를 처리하는 방법에 대해 생각할 수 있습니다 . 또한 값이 정수가 아닌 경우 부동 소수점 반올림 오류를 처리하는 방법 (예 : total = 0.3및로 시도 monthlyPayment = 0.1).
Ilmari Karonen

@Graham 내가 다음 문에 반응 ... 그 반응하지 않았다 "이에 대한 응답으로, API 팀은 기능 ... 변경" - 태아의 위치로 롤 업하고 흔들 시작 - 그것은 문제가 어디하지 않습니다 그 코드의 두 줄은 다른 형식의 새로운 API 호출이거나 호출자 쪽에서 수행됩니다. 이처럼 작동하는 API 호출을 변경하지 마십시오!
Baldrickk

답변:


99

구현을 보면 실제로 필요한 것은 하나가 아닌 3 가지 기능입니다.

원래 것 :

function getPaymentBreakdown(total, startDate, endDate) 

종료일 대신 개월 수를 제공하는 것 :

function getPaymentBreakdownByNoOfMonths(total, startDate, noOfMonths) 

월별 지불을 제공하고 종료일을 계산하는 것 :

function getPaymentBreakdownByMonthlyPayment(total, startDate, monthlyPayment) 

이제는 선택적 매개 변수가 더 이상 없으며 어떤 기능을 어떤 방법으로 어떤 목적으로 호출하는지 분명해야합니다. 주석에서 언급했듯이 엄격하게 유형이 지정된 언어에서는 함수 오버로딩을 사용하여 3 가지 기능을 반드시 이름이 아니라 서명으로 구별 할 수 있습니다.

다른 함수가 논리를 복제해야한다는 의미는 아닙니다. 내부적으로 이러한 함수가 공통 알고리즘을 공유하는 경우 "비공개"함수로 리팩토링해야합니다.

이와 같은 시나리오를 처리하는 일반적인 패턴이 있습니까?

좋은 API 디자인을 설명하는 "패턴"(GoF 디자인 패턴의 의미)이 없다고 생각합니다. 자체 설명 이름을 사용하면 매개 변수 수가 적은 함수, 직교 (= 독립) 매개 변수가있는 함수는 읽기 쉽고 유지 관리 가능하며 진화 가능한 코드를 만드는 기본 원칙 일뿐입니다. 프로그래밍의 모든 좋은 아이디어가 반드시 "디자인 패턴"인 것은 아닙니다.


24
실제로 코드의 "공통"구현은 단순히 getPaymentBreakdown(또는 실제로 그 중 하나) 될 수 있으며 다른 두 함수는 인수를 변환하고 호출합니다. 이 3 가지 중 하나의 완벽한 사본 인 개인 함수를 추가해야하는 이유는 무엇입니까?
Giacomo Alzetta

@GiacomoAlzetta : 가능합니다. 그러나 나는 확신 구현이 작전 기능 만 "반환"부분을 포함하는 공통 기능을 제공함으로써 간단하게, 대중 3 개 함수는 매개 변수를이 함수를 호출 할 것입니다 innerNumMonths, total하고 startDate. 3- 파라미터 기능이 작업을 수행 할 때 3 개가 거의 선택적 (여기서는 설정해야하는 것은 제외) 인 5 개의 매개 변수로 지나치게 복잡한 기능을 유지하는 이유는 무엇입니까?
Doc Brown

3
나는 "5 인수 함수를 유지하라"는 말은 아니었다. 나는 단지 당신이 어떤 공통된 논리를 가질 때이 논리는 사적 일 필요는 없다고 말하고있다 . 이 경우 매개 변수를 시작 날짜로 변환하기 위해 3 개의 함수를 모두 리팩토링 할 수 있으므로 공용 getPaymentBreakdown(total, startDate, endDate)함수를 일반적인 구현으로 사용할 수 있습니다 . 다른 도구는 적절한 총 / 시작 / 종료 날짜를 계산하여 호출합니다.
Giacomo Alzetta

@GiacomoAlzetta : 알았어요, 오해였습니다 getPaymentBreakdown. 질문에서 두 번째 구현에 대해 이야기하고 있다고 생각했습니다 .
Doc Brown

명시 적으로 'getPaymentBreakdownByStartAndEnd'라는 새 버전의 원래 메소드를 추가하고 모든 메소드를 제공하려는 경우 원래 메소드를 더 이상 사용하지 않습니다.
에릭

20

또한 나는 그것이 선례가 나쁜 것으로 생각하고 API와 관련해서는 안되는 책임을 추가합니다 (예 : SRP 위반). 추가 소비자 totalnumMonthsmonthlyPayment매개 변수 계산과 같은 더 많은 사용 사례를 지원하는 함수를 원한다고 가정 합니다. 이 기능은 시간이 지남에 따라 점점 더 복잡해질 것입니다.

당신은 정확히 맞습니다.

선호하는 것은 함수를 그대로 유지하고 대신 호출자가 endDate 자체를 계산하도록 요구하는 것입니다. 그러나, 내가 틀렸을 수도 있고 변경 사항이 API 기능을 설계하는 데 적합한 방법인지 궁금합니다.

발신자 코드가 관련없는 보일러 플레이트로 오염되기 때문에 이것은 이상적이지 않습니다.

또는 이와 같은 시나리오를 처리하기위한 일반적인 패턴이 있습니까?

와 같은 새로운 유형을 소개합니다 DateInterval. 의미가있는 생성자를 추가하십시오 (시작 날짜 + 종료 날짜, 시작 날짜 + num 개월 등). 시스템 전체의 날짜 / 시간 간격을 표현하기위한 공통 통화 유형으로 이것을 채택하십시오.


3
@DocBrown Yep. 이러한 경우 (Ruby, Python, JS) 정적 / 클래스 메소드 만 사용하는 것이 일반적입니다. 그러나 그것은 구현 세부 사항이며, 특히 내 대답의 요점과 관련이 없다고 생각합니다 ( "유형 사용").
알렉산더

2
그리고이 아이디어는 슬프게도 세 번째 요구 사항 인 시작 날짜, 총 지불 및 월별 지불로 한계에 도달합니다. 함수는 돈 매개 변수에서 DateInterval을 계산합니다-화폐 금액을 날짜 범위에 넣지 않아야합니다 ...
팔코

3
@DocBrown은 "기존 함수에서 타입의 생성자로 문제를 이동시킬뿐"입니다. 예, 타임 코드는 타임 코드를 놓아야하는 곳에두기 때문에 머니 코드는 머니 코드가가는 곳에있을 수 있습니다. 간단한 SRP이므로 "단지"라고 말했을 때 얻는 결과가 확실하지 않습니다. 그것이 모든 기능이하는 일입니다. 코드를 없애지 않고 더 적절한 장소로 옮깁니다. 그게 뭐가 문제 야? "하지만 축하합니다, 적어도 5 명의 유권자들이 미끼를 가져갔습니다."이것은 당신이 생각한 것보다 훨씬 더 어리석은 소리입니다.
Alexander

@Falco 저에게 새로운 방법 인 것 같습니다 (이 지불 계산기 클래스에서는 그렇지 않습니다 DateInterval) :calculatePayPeriod(startData, totalPayment, monthlyPayment)
Alexander

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
유창한 인터페이스 자체 가 특정 작업을 더 쉽고 어렵게 만들지는 않습니다. 이것은 빌더 패턴과 비슷합니다.
VLAZ

8
다음과 같은 착신 전화를 방지해야하는 경우 구현이 다소 복잡 할 수 있습니다.forTotalAmount(1234).breakIntoPayments().byPeriod(2).monthly().withPaymentsOf(12.34).byDateRange(saleStart, saleEnd);
Bergi

4
개발자가 진정으로 자신의 발로 촬영하고 싶다면 @Bergi를 사용하는 더 쉬운 방법이 있습니다. 그럼에도 불구하고 여러분이 제시 한 예는 훨씬 더 읽기 forTotalAmountAndBreakIntoPaymentsByPeriodThenMonthlyWithPaymentsOfButByDateRange(1234, 2, 12.34, saleStart, saleEnd);
쉽습니다

5
@DanielCuadra 내가하려고 한 요점은 귀하의 답변이 3 개의 상호 배타적 인 매개 변수를 갖는 OP 문제를 실제로 해결하지 못한다는 것입니다. 빌더 패턴을 사용하면 호출을 더 읽기 쉽게 만들 수 있으며 사용자가 이해가되지 않는다는 사실을 인식 할 가능성이 높아지지만 빌더 패턴 만 사용하더라도 한 번에 3 개의 값을 계속 전달할 수는 없습니다.
Bergi

2
@ 팔코? 예, 가능하지만 더 복잡하며 이에 대한 언급은 없었습니다. 내가 본 더 일반적인 빌더는 단일 클래스로만 구성되었습니다. 답변이 작성기 코드를 포함하도록 편집되면 기꺼이 승인하고 다운 투표를 제거합니다.
Bergi

2

다른 언어에서는 named parameters를 사용 합니다. 이것은 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}).
Bergi

1
:대신 해서는 안 =됩니까?
Barmar

매개 변수 중 하나만 null이 아니거나 사전에 없는지 확인할 수 있습니다.
Mateen Ulhaq

1
@Bergi은 - 문법 자체가 무의미한 매개 변수를 전달하지 못하도록하지는 않지만 단순히 몇 가지 확인을하고 오류를 던질 수
slebetman을

@ Bergi 나는 결코 자바 스크립트 전문가가 아니지만 ES6의 Destructuring Assignment가 여기에 도움이 될 수 있다고 생각하지만 이것에 대한 지식은 매우 밝습니다.
그레고리 커리

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

받는 무슨 일 totalstartDate매개 변수?
Bergi

이것은 멋진 API처럼 보이지만이 네 가지 함수가 어떻게 구현 될지 상상할 수 있습니까? (변형 유형과 공통 인터페이스를 사용하면 매우 우아 할 수 있지만 염두에 두어야 할 것은 확실하지 않습니다).
Bergi

@Bergi 내 게시물을 편집했습니다
Vinz243

0

그리고 차별화 된 공용체 / 대수 데이터 형식이있는 시스템을 사용하는 경우에는 다음과 같이 전달할 수 TimePeriodSpecification있습니다.

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

그런 다음 실제로 구현하지 못하는 문제는 발생하지 않습니다.


이것은 분명히 이와 같은 유형을 가진 언어로 이것을 접근하는 방법입니다. 나는 언어에 무관심한 질문을 유지하려고 노력했지만 어쩌면 어떤 경우에는 이와 같은 접근법이 가능하기 때문에 사용되는 언어를 고려해야합니다.
CalMlynarczyk
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.