有处理冲突的功能参数的模式吗?


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复杂化。现在,调用者需要有关隐藏与功能的实现启发式担心在确定哪些参数采取偏好被用来计算日期范围(即根据优先顺序monthlyPaymentnumMonthsendDate)。如果调用者不注意函数签名,则他们可能会发送多个可选参数,并对endDate被忽略的原因感到困惑。我们确实在功能文档中指定了此行为。

另外,我觉得它树立了一个不好的先例,并增加了它不应该关注的API责任(即违反SRP)。假设额外的消费者想要的功能,以支持更多的使用情况,如计算totalnumMonthsmonthlyPayment参数。随着时间的流逝,该功能将变得越来越复杂。

我的喜好是保持功能不变,而是要求调用者endDate自行计算。但是,我可能是错的,并且想知道所做的更改是否是设计API函数的可接受方法。

或者,是否存在用于处理此类情况的通用模式?我们可以在API中提供其他高阶函数来包装原始函数,但这会使API膨胀。也许我们可以添加一个额外的标志参数来指定在函数内部使用哪种方法。


79
“最近,此API的消费者希望[提供]月数而不是结束日期”-这是一个无聊的请求。他们可以将一两个月的代码中的月份数转换为正确的结束日期。
格雷厄姆

12
看起来像是Flag Argument反模式,我还建议拆分为几个函数
njzk2

2
作为一个侧面说明,有可以接受的参数相同类型和数目,并产生基于这些非常不同的结果的功能-看Date-你可以提供一个字符串,它可以被解析,以确定日期。但是,以这种方式处理参数也可能非常挑剔,并可能产生不可靠的结果。再见Date。做正确的事并非不可能-Moment会更好地处理它,但是无论如何使用都会很烦人。
VLAZ

稍微切线,您可能想考虑如何处理monthlyPayment给定但total不是整数的情况。如果不能保证这些值是整数(例如,使用total = 0.3和,请尝试使用它),以及如何处理可能的浮点舍入错误monthlyPayment = 0.1
Ilmari Karonen

@Graham我对此没有反应...我对下一条语句“对此做出了反应,API团队更改了功能...” - 向上滚动到胎儿位置并开始摇摆 -不管在哪里这一行或两行代码要么是使用不同格式的新API调用,要么是在调用者端完成的。只是不要更改这样的有效API调用!
Baldrickk

Answers:


99

看到实现之后,在我看来,您真正需要的是3种不同的功能,而不是一种:

原始的一个:

function getPaymentBreakdown(total, startDate, endDate) 

提供月数而不是结束日期的数字:

function getPaymentBreakdownByNoOfMonths(total, startDate, noOfMonths) 

提供月付款并计算结束日期的人:

function getPaymentBreakdownByMonthlyPayment(total, startDate, monthlyPayment) 

现在,不再有可选参数,并且应该很清楚地将哪个函数称为方法以及用于哪个目的。如评论中所提到的,在严格类型化的语言中,如果不混淆功能的目的,也可以利用函数重载,不一定要通过名称,而是通过签名来区分这三个不同的函数。

请注意,不同的功能并不意味着您必须重复任何逻辑-在内部,如果这些功能共享一个通用算法,则应将其重构为“私有”功能。

是否有处理这种情况的通用模式

我认为没有描述好的API设计的“模式”(就GoF设计模式而言)。使用自描述名称,具有较少参数的函数,具有正交(=独立)参数的函数只是创建可读,可维护和可演化代码的基本原理。并非编程中的每个好主意都一定是“设计模式”。


24
实际上,代码的“通用”实现可以简单地实现getPaymentBreakdown(或实际上是这3种实现中的任何一种),而其他两个函数只需转换参数并调用它即可。为什么要添加私有函数,这是这三个函数之一的完美副本?
Giacomo Alzetta

@GiacomoAlzetta:这是可能的。但我敢肯定的实施将成为通过提供含有有机磷农药功能的只有“回归”部分常见功能简单,让市民3个函数调用与参数此功能innerNumMonthstotalstartDate。当三参数函数也能完成这项工作时,为什么还要保留一个带有5个参数的复杂函数,其中3个几乎是可选的(必须设置一个除外)?
布朗

3
我的意思不是说“保留5参数函数”。我只是说,当您有一些通用逻辑时,此逻辑不必是私有的。在这种情况下,可以重构所有3个函数,以将参数简单地转换为开始日期,因此您可以将public getPaymentBreakdown(total, startDate, endDate)函数用作通用实现,另一个工具将简单地计算合适的总计/开始日期/结束日期并调用它。
Giacomo Alzetta

@GiacomoAlzetta:好的,这是一个误解,我以为您在谈论问题中的第二个实现getPaymentBreakdown
布朗

如果要提供所有这些方法,我将尽可能添加一个新版本的原始方法(明确地称为“ getPaymentBreakdownByStartAndEnd”)并弃用原始方法。
埃里克

20

另外,我觉得它树立了一个不好的先例,并增加了它不应该关注的API责任(即违反SRP)。假设额外的消费者想要的功能,以支持更多的使用情况,如计算totalnumMonthsmonthlyPayment参数。随着时间的流逝,该功能将变得越来越复杂。

你是完全正确的。

我的喜好是保持功能不变,而是要求调用者自己计算endDate。但是,我可能是错的,并且想知道所做的更改是否是设计API函数的可接受方法。

这也不是理想的选择,因为调用者代码将被无关的样板污染。

或者,是否存在用于处理此类情况的通用模式?

引入一种新的类型,例如DateInterval。添加任何有意义的构造函数(开始日期+结束日期,开始日期+ num月,等等)。将其用作表示系统中日期/时间间隔的通用货币类型。


3
@DocBrown是的。在这种情况下(Ruby,Python,JS),习惯只使用静态/类方法。但这是一个实现细节,我认为与我的回答没有特别关系(“使用类型”)。
亚历山大

2
这个想法可悲地达到了第三个要求:开始日期,总付款和每月付款-函数将根据货币参数计算DateInterval-您不应将货币金额放入日期范围...
法尔科

3
@DocBrown“仅将问题从现有函数转移到类型的构造函数”是的,它将时间代码放在应该放置时间代码的位置,以便货币代码可以放在应该放置货币代码的位置。它是简单的SRP,所以当您说它“仅”解决问题时,我不确定您要获得什么。这就是所有功能的作用。他们不会使代码消失,而是将其移至更合适的位置。你怎么了 “但是我很祝贺,至少有5个支持者抓住了诱饵。”这听起来比我想像的(希望)更加愚蠢。
亚历山大

@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
流利的界面本身不会使任何特定的任务变得容易或困难。这似乎更像是Builder模式。
VLAZ

8
但是,如果您需要防止像forTotalAmount(1234).breakIntoPayments().byPeriod(2).monthly().withPaymentsOf(12.34).byDateRange(saleStart, saleEnd);
Bergi

4
如果开发人员确实希望自己站起来,那就有@Bergi的简便方法。不过,您放置的示例比forTotalAmountAndBreakIntoPaymentsByPeriodThenMonthlyWithPaymentsOfButByDateRange(1234, 2, 12.34, saleStart, saleEnd);
DanielCuadra

5
@DanielCuadra我想说明的一点是,您的回答并不能真正解决具有3个互斥参数的OP问题。使用构建器模式可能会使调用更具可读性(并提高了用户注意到该调用没有意义的可能性),但仅使用构建器模式并不能阻止他们仍然一次传递3个值。
Bergi

2
@Falco会吗?是的,这是可能的,但更为复杂,答案没有提及。我见过的更常见的构建器仅包含一个类。如果答案经过编辑以包含建造者的代码,我将很乐意认可并删除我的不赞成意见。
Bergi

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

1
它不应该是:不是=
Barmar

我猜您可以检查只有一个参数为非null(或不在字典中)。
Mateen Ulhaq

1
@Bergi-语法本身不会阻止用户传递不必要的参数,但您可以简单地进行一些验证并抛出错误
slebetman

@Bergi我绝不是Java语言专家,但是我认为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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.