A F# 中相当惯用的方法是使用签名文件隐藏实施细节 http://fsharpforfunandprofit.com/posts/designing-with-types-single-case-dus,但一如既往,这涉及到权衡。
想象一下您已经这样定义了模型:
module MyDomainModel
type CounterValues = { Values : int list; IsCorrupt : bool }
let createCounterValues values =
{
Values = values |> List.map (max 0)
IsCorrupt = values |> List.exists (fun x -> x < 0)
}
let values cv = cv.Values
let isCorrupt cv = cv.IsCorrupt
请注意,除了create检查输入的函数,该模块还包含以下访问器函数Values
and IsCorrupt
。这是必要的,因为下一步。
到目前为止,定义在MyDomainModel
模块是公共的。
但是,现在您添加一个签名文件 (a .fsi
file) before the .fs
文件包含MyDomainModel
。在签名文件中,您只放置想要发布到外界的内容:
module MyDomainModel
type CounterValues
val createCounterValues : values : int list -> CounterValues
val values : counterValues : CounterValues -> int list
val isCorrupt : counterValues : CounterValues -> bool
请注意,声明的模块名称是相同的,但类型和函数仅在抽象中声明。
Because CounterValues
被定义为一种类型,但没有任何特定的结构,任何客户端都无法创建它的实例。换句话说,这不能编译:
module Client
open MyDomainModel
let cv = { Values = [1; 2]; IsCorrupt = true }
编译器抱怨“记录标签‘Values’未定义”。
另一方面,客户端仍然可以访问签名文件定义的功能。这编译:
module Client
let cv = MyDomainModel.createCounterValues [1; 2]
let v = cv |> MyDomainModel.values
let c = cv |> MyDomainModel.isCorrupt
以下是 FSI 的一些示例:
> createCounterValues [1; -1; 2] |> values;;
val it : int list = [1; 0; 2]
> createCounterValues [1; -1; 2] |> isCorrupt;;
val it : bool = true
> createCounterValues [1; 2] |> isCorrupt;;
val it : bool = false
> createCounterValues [1; 2] |> values;;
val it : int list = [1; 2]
缺点之一是保留签名文件会产生开销(.fsi
)和实现文件(.fs
) 同步中。
另一个缺点是客户端无法自动访问记录的命名元素。相反,您必须定义和维护访问器函数,例如values
and isCorrupt
.
尽管如此,这并不是 F# 中最常见的方法。更常见的方法是提供必要的函数来动态计算此类问题的答案:
module Alternative
let replaceNegatives = List.map (max 0)
let isCorrupt = List.exists (fun x -> x < 0)
如果列表不太大,则动态计算此类答案所涉及的性能开销可能小到足以忽略(或者可以通过记忆来解决)。
以下是一些使用示例:
> [1; -2; 3] |> replaceNegatives;;
val it : int list = [1; 0; 3]
> [1; -2; 3] |> isCorrupt;;
val it : bool = true
> [1; 2; 3] |> replaceNegatives;;
val it : int list = [1; 2; 3]
> [1; 2; 3] |> isCorrupt;;
val it : bool = false