0

我喜欢编写纯函数的想法,但是我很难理解将它们组合起来以生成可测试代码的方法。我习惯于提取类然后适当地存根,并且觉得我缺少对函数式编程的一些关键见解。

这是我从当前面临的问题中精简的示例。

我想列出一个日期列表并将其过滤为符合“机会”标准的日期。

在 C# 中,它看起来像:

    static List<List<DateTime>> Opportunities(List<List<DateTime>> dates)
    {
        return dates.Where(ds => HasOpportunity(ds)).ToList();
    }

    static bool HasOpportunity(List<DateTime> dates)
    {
        var threshold = 0.05D;

        var current = OpportunityProbability(dates, DateTime.Now);
        var previous = OpportunityProbability(dates, DateTime.Now.Subtract(TimeSpan.FromDays(30)));

        return previous >= threshold && current < threshold;
    }

    static double OpportunityProbability(List<DateTime> dates, DateTime endDate)
    {
        // does lots of math
        return 0.0D;
    }

所以在提示我们OpportunityProbability知道如何测试。我遇到的麻烦是在HasOpportunity链条上(Opportunities)。

我知道如何测试的唯一方法HasOpportunity是存根,OpportunityProbability但我做不到。而且我不想创建假数据来满足OpportunityProbability测试的设计HasOpportunity。因此,即使这两个函数都是纯函数,它也是不可测试的,我觉得它的设计很糟糕。

因此我觉得我正在设计糟糕的功能代码:)

我关心HasOpportunity的主要是布尔测试。给定两个双精度数和一个阈值,进行比较并返回结果。为了获得这两个双打,它使用了一个需要日期列表和日期列表的函数。这导致HasOpportunity还负责确定日期(DateTime.Now和前 30 天)。也许我可以把它分开:

    static bool HasOpportunity(double probability1, double probability2)
    {
        var threshold = 0.05D;

        return probability2 >= threshold && probability1 < threshold;
    }

所以这显然是可测试的。我什至可以提高门槛:

    static bool HasOpportunity(double threshold, double probability1, double probability2)
    {
        return probability2 >= threshold && probability1 < threshold;
    }

所以这更通用。

我这样做时遇到的问题是我刚刚将事情转移到Opportunities

    static List<List<DateTime>> Opportunities(List<List<DateTime>> dates)
    {
        return dates.Where(ds => {
            var current = OpportunityProbability(ds, DateTime.Now);
            var previous = OpportunityProbability(ds, DateTime.Now.Subtract(TimeSpan.FromDays(30)));
            return HasOpportunity(0.05D, current, previous);
        }).ToList();
    }

这是我不知道下一步要采取的地方。

功能霸主有什么想法吗?帮我用 C# 编写 F#,在此先感谢!

更新

所以再迈出一步,我可以得到:

    static List<List<DateTime>> Opportunities(double threshold, DateTime currentDate, DateTime previousDate, List<List<DateTime>> dates)
    {
        return dates.Where(ds => {
            var current = OpportunityProbability(ds, currentDate);
            var previous = OpportunityProbability(ds, previousDate);
            return HasOpportunity(threshold, current, previous);
        }).ToList();
    }

所以我仍然不知道如何测试这个,但很好的是这个函数的参数最终定义了机会是什么:

  • 临界点
  • 第一次约会
  • 第二次约会

然后给出一个日期列表,它可以给你机会。

4

2 回答 2

2

你考虑过使用高阶函数吗?将 OpportunityProbability 函数传入 HasOpportunity。

static List<List<DateTime>> Opportunities(List<List<DateTime>> dates)
{
    return dates.Where(ds => HasOpportunity(ds, OpportunityProbability, OpportunityProbability)).ToList();
}

static bool HasOpportunity(List<DateTime> dates, Func<List<DateTime>, DateTime, double> currentProb, Func<List<DateTime>, DateTime, double> prevProb)
{
    var threshold = 0.05D;

    var current = currentProb(dates, DateTime.Now);
    var previous = prevProb(dates, DateTime.Now.Subtract(TimeSpan.FromDays(30)));

    return previous >= threshold && current < threshold;
}

static double OpportunityProbability(List<DateTime> dates, DateTime endDate)
{
    // does lots of math
    return 0.0D;
}

现在您可以测试 OpportunityProbability 和 HasOpportunity 彼此独立(在 HasOpportunity 的情况下,您“存根”第二个和最后一个参数。如果您想要更多的分离,您也可以将 OpportunityProbability 传递给 Opportunities。

于 2013-06-16T17:36:10.513 回答
0

我认为你应该加入一些古老的面向对象,并尊重单一职责模式。一种可能的方法是创建类:

  • OpportunityCalculator用方法double OpportunityProbability(List<DateTime> dates, DateTime endDate)

  • OpportunityFilter用方法bool HasOpportunity(double threshold, double probability1, double probability2)

这些类可以独立测试:

  • OpportunityCalculator抽象出复杂的数学。
  • 在测试OpportunityFilter时,您可以存根OpportunityCalculator。您的测试将围绕这样一个事实,即计算器将使用正确的参数进行两次咨询。
于 2013-06-16T17:10:51.440 回答