设计或代码模式从函数返回多个可选值

时间:2016-12-13 15:11:41

标签: c# design-patterns f# c#-to-f#

我一直在C#中编写几个实用的,可重用的函数。许多函数返回多个值,我使用out或ref参数。许多功能还有其他信息,这些信息可能对某些呼叫者(但不是所有呼叫者)有用。

例如,读取CSV文件的功能可能包含其他信息,例如否。空白行,没有。具有重复值的行和其他一些统计信息。

其他信息还可能包括警告,消息等。

并非每个调用者都会对此信息感兴趣,因此我不希望将所有这些包含在out,ref或Tuples中,这将使调用者必须声明所有此类预期变量。

我只是想知道是否有办法让呼叫者可以获得其他信息,以便呼叫者可以选择或检索它感兴趣的一些可选附加信息。

例如,Func 1可以调用Func B.调用它后,它将获得所有标准返回值。另外,它可以调用类似FuncB.GetAdditionalInfo(infoType)的东西而不再执行Func B吗?

可以使用一个类来设计它,该类用作存储所有可选值的中介,然后根据请求将它们返回给调用者;但我希望它足够通用,可用于我的所有实用程序功能。

一种可能性是Func B将所有这些存储在某种全局变量中,如果需要,调用者可以访问这些变量。但是如果一个实用程序类有几个这样的可恢复函数,我将需要有这么多的公共变量,以获取每个函数的附加信息!

我现在在.NET 4.5上。这有设计模式吗?我很乐意知道F#中是否有一个很好的解决方案。

另外,我想避免过多的重载版本来实现这一目标! 感谢。

3 个答案:

答案 0 :(得分:3)

我不反对向您展示理想的实现,但这是一个对我有意义的实现。设计两种不同的数据结构:一种表示函数接受的选项,另一种表示函数返回的选项。例如:

public class Helper
{

    // General cover-it-all implementation that accepts an option object
    // and analyzes based on the flags that are set in it
    public static CSVStatistics AnalyzeCSV(string file, CSVAnalysisOptions options)
    {
        // define what we are analysing by reading it from the
        // from the options object and do your magic here
    }

    // Specific implementation that counts only blank lines
    public static long CountBlankLines(string file)
    {
        var analysisResult = AnalyseCSV(file, new CSVAnalysisOptions
        {
            IsCountingBlanks = true
        });
        //I'm not doing a null check here, because I'm settings the
        //flag to True and therefore I expect there to be a value
        return analysisResult.BlanksCount.Value;
    }
}
// Analysis options structure
public struct CSVAnalysisOptions
{
    public bool IsCountingBlanks { get; set; }
    public bool IsCountingDuplicates { get; set; }
    public bool IsCountingOther { get; set; }
}

// Analysis results structure
public struct CSVStatistics
{
    public long TotalLineCount { get; set; }
    public long? BlanksCount { get; set; }
    public long? DuplicatesCount { get; set; }

}

在上面的例子中,CountBlankLines是一个特定的实现,只计算空行并且作为" sugar"这简化了调用,而AnalyzeCSV是实际进行计数的方法。另外,请注意CSStatistics结构如何具有可空long个。这将允许您检查值是否为空,因此知道它实际上没有被分析而不是输出零(这是一个可能的值)。

CSVAnalysisOptions结构也可以用位标志代替,你可以在这里阅读它们 - https://developer.mozilla.org/en-US/docs/Web/API/Window/parent

答案 1 :(得分:3)

我觉得你要做的就是构建一个非常的大块API,可以一次完成很多事情。一般来说,我们不喜欢粗糙的API,因为它们会变得复杂,特别是如果API中的选项之间的交互存在副作用或异常怪癖。

老实说,这样做的最好方法是创建一个更加可爱的API,其中每个调用都做一件事,做得对,做得好。

执行此操作时,代码最终会更容易进行因子和单元测试。

这并不是说没有引起适度的厚度,但这应该是合乎逻辑的。

例如,如果你要破解图像文件来解码,比如PNG或JPG,你需要预先设置图像的宽度,高度,分辨率和颜色类型。一次性抓住所有这些是完全合理的。您需要立即挖掘元数据信息或颜色配置文件吗?可能不是。

因此,有一个调用可以返回并汇总所有基本图像信息,然后单独调用以获取其余信息。

"但表现!"你说,"性能怎么样?!"

简单。你测量它,看看会发生什么。几年前,我写了一个PNG解码器,不像libpng顺序读取块,我认为在前面构建一个映射文件每个块的数据库会更容易,然后引用该数据库来查找任何给定的块。令人惊讶的是,这无法显着影响性能,并使消费代码更易于阅读和维护。

让事情被多次调用,如果存在性能问题,请弄清楚如何解决它。通常,您使用对获取信息的对象专用的缓存或会话执行此操作。

您所描述的内容听起来既不容易阅读也不易维护,更不用说测试了。

答案 2 :(得分:0)

这是一个可能适合您的用例的F#模式。它与Argu库用于命令行参数的模式几乎相同:您声明一个discriminated union,其中包含所有可能的"标记"你的函数可能想要返回(我把" flags"用引号,因为它们中的一些可能不只是布尔值),然后你的函数可以返回这些值的列表。如果有几十个,那么一个集合可能是值得的(因为列表必须线性搜索),但是如果你不希望返回超过七个或八个这样的标志,那么集合的额外复杂性就是& #39;值得,你也可以使用一个清单。

一些F#代码说明了如何使用这种模式,使用虚拟函数来实现业务逻辑:

type Notifications
    | InputWasEmpty
    | OutputWasEmpty
    | NumberOfBlankLinesInOutput of int
    | NumberOfDuplicateLinesInOutput of int
    | NumberOfIgnoredErrors of int
    // Whatever else...

type ResultWithNotifications<'a> = 'a * Notifications list
// The syntax "TypeA * TypeB" is F# syntax for Tuple<TypeA,TypeB>
// And the 'a is F# syntax for a generic type

type outputRecord = // ... insert your own data type here

// Returns the filename of the output file, plus various notifications
// that the caller can take action on if they want to
let doSomeProcessing data : ResultWithNotifications<outputRecord list>
    let mutable notifications = []
    let outputFileName = getSafeOutputFilename()
    if List.isEmpty data then
        notifications <- InputWasEmpty :: notifications
    let output = data |> List.choose (fun record ->
        try
            let result = record |> createOutputRecordFromInputRecord
            Some result
        except e
            eprintfn "Got exception processing %A: %s" record (e.ToString())
            None
    )
    if List.isEmpty output then
        notifications <- OutputWasEmpty :: notifications
    if List.length output < List.length data then
        let skippedRecords = List.length data - List.length output
        notifications <- (NumberOfIgnoredErrors skippedRecords) :: notifications
    // And so on. Eventually...
    output |> writeOutputToFilename outputFileName
    outputFileName, notifications  // Function result

希望F#代码在没有解释的情况下是可理解的,但如果上面有任何不清楚的地方,请告诉我,我会尝试解释。