在F#中工作并实现“服务”模式,例如Web API或数据库或USB小工具的包装器时,这样做的惯用方法是什么?我倾向于使用例如与{C#中的IDatabase
和Database
,ITwitter
和Twitter
,ICamera
和Camera
接口和类一样,可以轻松进行测试模拟。但我不想在F#中编写“C#”,如果这不是标准的方法。
我考虑使用DU来代表例如Camera | TestCamera
,但这意味着将所有模拟代码放入生产代码库中,这听起来很糟糕。但也许这只是我的OO背景说法。
我还考虑过将IDatabase
作为功能记录。我仍然对这个想法持开放态度,但似乎有点做作。此外,它排除了使用IoC控制器或任何“类MEF”插件功能的想法(至少我知道)。
“惯用F#”的做法是什么?只需遵循C#服务模式吗?
答案 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 Principle和Interface 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#允许您使用对象表达式语法内联创建实现。