我正在研究原型使用文档数据库(目前MongoDB,可能会发生变化)并发现.NET驱动程序有点痛苦,所以我想我会用Repository模式抽象数据访问。这样可以很容易地将我正在使用的任何驱动程序(NoRM,mongodb-csharp,simple-mongob)替换为你的杀手f#mongodb驱动程序,当它准备就绪时不会吮吸。
我的问题在于添加操作。这会对数据库产生一些副作用,因此对 All 的后续调用将会有所不同。我应该关心吗?在C#传统上我不会,但我觉得在F#我应该。
这是通用存储库接口:
type IRepository<'a> =
interface
abstract member All : unit -> seq<'a>
// Add has a side-effect of modifying the database
abstract member Add : 'a -> unit
end
以下是MongoDB实现的外观:
type Repository<'b when 'b : not struct>(server:MongoDB.IMongo,database) =
interface IRepository<'b> with
member x.All() =
// connect and return all
member x.Add(document:'b) =
// add and return unit
在整个应用程序中,我将使用IRepository,以便更改驱动程序和可能的数据库。
调用All很好,但是添加我希望的是而不是返回单元,返回一个新的存储库实例。类似的东西:
// Add has a side-effect of modifying the database
// but who cares as we now return a new repository
abstract member Add : 'a -> IRepository<'a>
问题是,如果我调用Get,然后添加,原始存储库仍会返回所有文档。例如:
let repo1 = new Repository<Question>(server,"killerapp") :> IRepository<Question>
let a1 = repo1.All()
let repo2 = repo1.Add(new Question("Repository pattern in F#"))
let a2 = repo2.All()
理想情况下,我希望a1和a2的长度不同,但它们与数据库中的长度相同。应用程序工作,用户可以问他们的问题,但程序员不知道为什么它返回一个新的IRepository。
那么我应该尝试在类型设计中处理Add对数据库的副作用吗?其他人如何解决这个问题,你是使用Repository还是这样的接口类还是有更好的功能方法?
答案 0 :(得分:4)
看起来您正在将不可变性应用于影响外部世界状态的函数。无论F#实现如何,您如何看待它在MongoDB级别工作?您如何阻止repo1
看到repo2
所做的任何更改?如果某些其他流程影响数据库会发生什么情况 - 在这种情况下,repo1
和repo2
都会发生变化吗?
换句话说,假设System.Console
的实现就像这样工作。如果Console.Out.WriteLine
始终返回一个新的不可变对象,它将如何与Console.In.ReadLine
的调用进行交互?
编辑tl; dr:不要这样做。有时副作用很好。
答案 1 :(得分:2)
我认为对一个天生可变的类型(例如数据库)拥有一个不可变的接口是没有意义的。但是,您可能希望将功能拆分为可变数据库类型(在您的情况下为IRepository<'a>
)和一组不可变更改(例如ChangeSet<'a>
)。结果可能类似于:
type ChangeSet<'a> = ... //'
module ChangeSet = begin //'
let empty = ... //'
let add a c = ... //'
...
end
type IRepository<'a> = //'
abstract GetAll : unit -> seq<'a> //'
abstract ApplyChanges : ChangeSet<'a> -> unit //'
type Repository<'a> = ... //'
let repo = new Repository<Question>(...)
let changes =
ChangeSet.empty
|> ChangeSet.add (Question "Repository pattern in F#")
|> ChangeSet.add (Question "...")
repo.ApplyChanges changes
let results = repo.GetAll()
答案 2 :(得分:0)
你可以将它包装在一个计算表达式中,使它看起来很纯净。您甚至可以使用代码进一步扩展它以处理超时和故障服务器。我是这个概念的新手,所以如果有些事情看起来不合适,专家可以给我上学。
我认为如果您通过线程而不仅仅是一个存储库,这个概念会更有用,但我想保持简单。
type IRepository<'a> = //'
abstract member All : unit -> seq<'a> //'
abstract member Add : 'a -> unit //'
abstract member Get : int -> 'a //'
type Rep<'a, 'b> = IRepository<'a> -> 'b //'
type RepositoryBuilder() =
member x.Bind (f:Rep<'a, 'b>, g:'b -> Rep<'a, 'c>) rep = g (f rep) rep //'
member x.Delay (f:unit -> Rep<'a, 'b>) = f () //'
member x.Return v r = v
member x.ReturnFrom f = f
member x.Zero () = ()
let rep = RepositoryBuilder()
let action (action:_->unit) repository =
action repository
let func (func:Rep<_, _>) repository =
func repository
type Person = {
id:int
name:string
age:int
finalized:bool
}
let addPeople = rep {
do! action(fun r -> r.Add { id = 1; name = "Jim"; age = 45; finalized = false })
do! action(fun r -> r.Add { id = 2; name = "Bob"; age = 32; finalized = false })
do! action(fun r -> r.Add { id = 3; name = "Sue"; age = 58; finalized = false })
do! action(fun r -> r.Add { id = 5; name = "Matt"; age = 11; finalized = false })
}