简体   繁体   English

F#中的存储库模式

[英]Repository pattern in F#

I'm working on a prototype to use a document database (currently MongoDB, may change) and found the .NET drivers a bit of a pain, so I thought I would abstract the data access with the Repository pattern. 我正在研究原型使用文档数据库(目前MongoDB,可能会改变),并发现.NET驱动程序有点痛苦,所以我想我会用Repository模式抽象数据访问。 This should make it easy to swap out whatever driver I'm using now (NoRM, mongodb-csharp, simple-mongob) with your killer f# mongodb driver that doesn't suck when it's ready. 这应该可以很容易地将你正在使用的任何驱动程序(NoRM,mongodb-csharp,simple-mongob)替换为你的杀手f#mongodb驱动程序,它在准备就绪时不会吮吸

My question is around the Add operation. 我的问题是关于添加操作。 This is going to have some side affect to the database and thus subsequent calls to All will be different. 这将对数据库产生一些副作用,因此对All的后续调用将会有所不同。 Should I care? 我应该关心吗? In C# traditionally I wouldn't, but I feel that in F# I should. 在C#传统上我不会,但我觉得在F#我应该。

Here is the generic repository interface: 这是通用存储库接口:

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

And here is how a MongoDB implementation looks: 以下是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

Throughout the app I will use IRepository, making it easy to change drivers and potentially databases. 在整个应用程序中,我将使用IRepository,从而可以轻松更改驱动程序和可能的数据库。

Calling All is fine, but with Add what I was hoping was instead of returning unit, return a new repository instance. 调用All很好,但是添加我希望的是而不是返回单元,返回一个新的存储库实例。 Something like: 就像是:

        // 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>

The problem is that if I call Get, then Add, the original repository still returns all the documents. 问题是如果我调用Get,然后添加,原始存储库仍然会返回所有文档。 Example: 例:

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()

Ideally I want length of a1 and a2 to be different, but they are the same as they both hit the database. 理想情况下,我希望a1和a2的长度不同,但它们与数据库中的长度相同。 The application works, users can ask their question, but the programmer is left wondering why it returns a new IRepository. 应用程序工作,用户可以问他们的问题,但程序员不知道为什么它返回一个新的IRepository。

So should I be trying to handle the side-effect from Add on the database in the design of the types? 那么我应该尝试在类型设计中处理Add对数据库的副作用吗? How would others go about this, do you use a Repository or some interface class like this or have some better functional approach? 其他人如何解决这个问题,您是使用Repository还是这样的接口类还是有更好的功能方法?

It looks like you're applying immutability to functions that affect state in the outside world. 看起来你正在将不可变性应用于影响外部世界状态的函数。 Regardless of the F# implementation, how would you see this working at the MongoDB level? 无论F#实现如何,您如何看待它在MongoDB级别工作? How would you prevent repo1 from seeing any changes that repo2 makes? 你如何防止repo1看到repo2发生的任何变化? What happens if some other process affects the database -- do both repo1 and repo2 change in this case? 如果某些其他进程影响数据库会发生什么 - 在这种情况下repo1repo2发生变化吗?

To put it another way, imagine an implementation of System.Console that worked like this. 换句话说,想象一下像这样工作的System.Console的实现。 If Console.Out.WriteLine always returned a new immutable object, how would it interact with calls to Console.In.ReadLine ? 如果Console.Out.WriteLine总是返回一个新的不可变对象,它将如何与对Console.In.ReadLine调用进行交互?

Edit tl;dr: Don't do this. 编辑tl;博士:不要这样做。 Sometimes side effects are fine. 有时副作用很好。

I don't think that it makes sense to have an immutable interface to an innately mutable type (such as a database). 我不认为拥有一个天生可变类型(如数据库)的不可变接口是有意义的。 However, you might want to split the functionality into a mutable database type ( IRepository<'a> in your case) and an immutable set of changes (such as ChangeSet<'a> , for example). 但是,您可能希望将功能拆分为可变数据库类型(在您的情况下为IRepository<'a> )和一组ChangeSet<'a>改(例如, ChangeSet<'a> )。 The result might look something like: 结果可能如下所示:

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()

You could wrap it in a computational expression to make it seem pure. 你可以将它包装在一个计算表达式中,使它看起来很纯净。 You could even extend it further with code to handle timeouts and downed servers. 您甚至可以使用代码进一步扩展它以处理超时和故障服务器。 I am new to the concept so if the experts could school me if something seems inappropriate. 我是这个概念的新手,所以如果有些事情看起来不合适,专家可以跟我上学。

I think this concept would be much more useful if you were threading through more than just a repository but I wanted to keep it simple. 我认为如果你通过线程而不仅仅是一个存储库,这个概念会更有用,但我想保持简单。

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 }) 
}  

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM