在F#中实现“服务”的惯用方法

时间:2015-09-14 17:24:15

标签: f#

在F#中工作并实现“服务”模式,例如Web API或数据库或USB小工具的包装器时,这样做的惯用方法是什么?我倾向于使用例如与{C#中的IDatabaseDatabaseITwitterTwitterICameraCamera接口和类一样,可以轻松进行测试模拟。但我不想在F#中编写“C#”,如果这不是标准的方法。

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

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

“惯用F#”的做法是什么?只需遵循C#服务模式吗?

3 个答案:

答案 0 :(得分:10)

正如在另一个答案中所提到的,在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

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

  

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

答案 1 :(得分:6)

虽然其他人写到在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意味着&#34;客户端[...]拥有抽象接口&#34; (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 -> #seq<Product>(因为categoryId可以是任何泛型类型'a)。

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

答案 2 :(得分:4)

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

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

我已经看到DU大致按照你所描述的方式使用(提供制作和虚假实现),但它也不适合我。

如果有的话,功能记录不是惯用的。他们是一个穷人将FP行为中的行为捆绑在一起的方式,但如果面向对象的语言做得恰到好处,它就会将行为捆绑在一起。接口只是一个更好的工具,特别是因为F#允许您使用对象表达式语法内联创建实现。