F#中的存储库模式

时间:2010-11-29 16:40:27

标签: .net f#

我正在研究原型使用文档数据库(目前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还是这样的接口类还是有更好的功能方法?

3 个答案:

答案 0 :(得分:4)

看起来您正在将不可变性应用于影响外部世界状态的函数。无论F#实现如何,您如何看待它在MongoDB级别工作?您如何阻止repo1看到repo2所做的任何更改?如果某些其他流程影响数据库会发生什么情况 - 在这种情况下,repo1repo2都会发生变化吗?

换句话说,假设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 }) 
}  
相关问题