【问题标题】:Idiomatic way to implement "services" in F#在 F# 中实现“服务”的惯用方式
【发布时间】:2015-09-14 17:24:14
【问题描述】:

在 F# 中工作并实现“服务”模式(例如围绕 Web API 或数据库或 USB 小工具的包装器)时,执行此操作的惯用方式是什么?我的倾向是使用例如IDatabaseDatabaseITwitterTwitterICameraCamera 接口和类,就像在 C# 中一样,允许轻松测试模拟。但如果这不是标准方法,我不想编写“F# 中的 C#”。

我考虑使用 DU 来表示例如Camera | TestCamera,但这意味着将所有模拟代码放入生产代码库,这听起来很可怕。但也许这只是我的 OO 背景。

我还考虑让IDatabase 成为函数记录。我仍然对这个想法持开放态度,但它似乎有点做作。此外,它排除了使用 IoC 控制器或任何“类似 MEF”的插件功能(至少我知道)的想法。

执行此操作的“惯用 F#”方式是什么?只需遵循 C# 服务模式即可?

【问题讨论】:

标签: f#


【解决方案1】:

正如另一个答案中提到的,在 F# 中使用接口很好,这可能是解决问题的好方法。但是,如果您使用更多面向函数转换的编程风格,则可能不需要它们。

理想情况下,大多数实际有趣的代码应该是不执行任何效果的转换,因此它们不调用任何服务。相反,他们只是接受输入并产生输出。让我演示一下如何使用数据库。

使用IDatabase 服务,您可能会这样写:

let readAveragePrice (database:IDatabase) = 
  [ for p in database.GetProducts() do
      if not p.IsDiscontinued then
        yield p.Price ]
  |> Seq.average

这样编写时,您可以提供IDatabase 的模拟实现来测试readAveragePrice 中的平均逻辑是否正确。不过,你也可以这样写代码:

let calculateAveragePrice (products:seq<Product>) = 
  [ for p in products do
      if not p.IsDiscontinued then
        yield p.Price ]
  |> Seq.average

现在您可以测试calculateAveragePrice 而无需任何模拟 - 只需给它一些您想要处理的示例产品!这将数据访问从核心逻辑推到外部。所以你有:

database.GetProducts() |> calculateAveragePrice // Actual call in the system
[ Product(123.4) ] |> calculateAveragePrice     // Call on sample data in the test

当然,这是一个简单的例子,但它说明了这个想法:

将数据访问代码推送到核心逻辑之外,并将核心逻辑保留为实现转换的纯函数。然后您的核心逻辑将很容易测试 - 给定示例输入,它们应该返回正确的结果!

【讨论】:

    【解决方案2】:

    虽然这里的其他人写道,在 F# 中使用接口并没有错,但我认为接口只不过是一种互操作机制,它使 F# 能够与用其他 .NET 语言编写的代码一起工作。

    当我用 C# 编写代码时,我遵循SOLID principles。当一起使用时,Single Responsibility PrincipleInterface Segregation Principle 最终应该是drive you towards defining as small-grained interfaces as possible

    因此,即使在 C# 中,设计合理的接口也应该只有一个方法

    public interface IProductQuery
    {
        IEnumerable<Product> GetProducts(int categoryId);
    }
    

    此外,Dependency Inversion Principle 暗示“客户 [...] 拥有抽象接口”(APPP,第 11 章),这意味着人为的方法捆绑是一种糟糕的设计无论如何。

    定义像上面IProductQuery这样的接口的唯一原因是,在C#中,接口仍然是多态性的最佳机制。

    另一方面,在 F# 中,没有理由定义单成员接口。 改用函数

    您可以将函数作为参数传递给其他函数:

    let calculateAveragePrice getProducts categoryId = 
        categoryId
        |> getProducts
        |> Seq.filter (fun p -> not p.IsDiscontinued)
        |> Seq.map (fun p -> p.Price)
        |> Seq.average
    

    在此示例中,您会注意到 getProducts 作为参数传递给 calculateAveragePrice 函数,甚至没有声明其类型。这与让客户端定义接口的原则完美契合:参数的类型是从客户端的使用中推断出来的。它的类型为'a -&gt; #seq&lt;Product&gt;(因为categoryId 可以是任何泛型类型'a)。

    calculateAveragePrice 这样的函数是 higher-order function,这是一个函数式的事情。

    【讨论】:

    • 我看到了推理和逻辑,但结论有问题。鉴于记录是get_ 方法的捆绑包,这是否意味着它们也是糟糕的设计?在哪里/为什么画线?不过,这可能是一段较长的对话。
    • 具体来说,以ICamera 为例,目前getNewPhoto camera config 必须调用多个camera.setWhatever cfg.whatevercamera.snap()camera.awaitFilecamera.download。将这些方法中的每一个作为 N 个单独的参数传递给getNewPhoto 似乎很笨拙。你会建议如何处理这样的事情?
    • (n.b. getNewPhoto 函数在许多不同的地方使用,所以我认为分解成更少参数的块并在任何地方内联它们是不明智的。)
    • 正如我在stackoverflow.com/q/32411667/126014 中所写的那样,如果您需要将所有这些函数作为参数传递给同一个函数,这可能表明该函数做得太多。当您结合 SRP 和 ISP 时,这在 SOLID 中也有等效项。
    • "记录是 get_ 方法的捆绑" 仅当您考虑 IL 级实现时,这才是正确的。记录是的包,而不是方法。那些get 方法本可以作为字段来实现,并且不会有任何区别。
    【解决方案3】:

    在 F# 中使用接口并没有错。

    教科书的 FP 方法是让客户端将实现组件逻辑的函数作为参数,但是随着这些函数数量的增长,这种方法不能很好地扩展。将它们组合在一起是一个好主意,因为它提高了可读性,并且为此使用接口是 .NET 中的一种惯用方式。特别是在定义明确的组件的边界上,这很有意义,我认为您引用的三个接口非常适合该描述。

    我看到 DU 大致按照您描述的方式使用(提供生产和假实现),但它也不适合我。

    如果有的话,函数的记录不是惯用的。在 FP 语言中,它们是一种将行为捆绑在一起的穷人方式,但是如果面向对象的语言做得对,那就是将行为捆绑在一起。接口只是完成这项工作的更好工具,尤其是因为 F# 允许您创建与对象表达式语法内联的实现。

    【讨论】:

    • 接受,因为它是第一个并且涵盖了主要案例;其他答案是基于这个的细节。
    猜你喜欢
    • 1970-01-01
    • 2013-05-01
    • 2014-07-03
    • 2012-08-11
    • 2013-08-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多