Шаблон репозитория в F #

Я работаю над прототипом для использования базы данных документов (в настоящее время MongoDB, может измениться) и обнаружил, что драйверы .NET немного больно, поэтому я подумал, что я бы абстрактным образом предоставлял доступ к данным с помощью шаблона репозитория. Это должно облегчить замену любого драйвера, который я использую сейчас (NoRM, mongodb-csharp, simple-mongob) с вашим драйвером killer f # mongodb, который не сосать, когда он готов.

Мой вопрос связан с операцией Add. Это будет иметь некоторое влияние на базу данных, и поэтому последующие вызовы для всех будут разными. Мне все равно? В С# традиционно я не хотел бы, но я чувствую, что в 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, упрощая изменение драйверов и потенциально баз данных.

Вызов Все в порядке, но с добавлением того, что я надеялся, вместо возврата единицы, верните новый экземпляр репозитория. Что-то вроде:

        // 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, то Add, исходный репозиторий все равно возвращает все документы. Пример:

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 в базе данных в дизайне типов? Как другие подумают об этом, используете ли вы репозиторий или какой-либо интерфейс, например, или используете более функциональный подход?

Ответ 1

Похоже, вы применяете неизменность к функциям, которые влияют на состояние во внешнем мире. Независимо от реализации F #, как бы вы видели, как это работает на уровне MongoDB? Как вы могли бы предотвратить repo1 от просмотра каких-либо изменений, которые делает repo2? Что произойдет, если какой-либо другой процесс повлияет на базу данных - измените ли в этом случае как repo1, так и repo2?

Другими словами, представьте себе реализацию System.Console, которая работает так. Если Console.Out.WriteLine всегда возвращал новый неизменяемый объект, как бы он взаимодействовал с вызовами Console.In.ReadLine?

Изменить tl; dr: Не делайте этого. Иногда побочные эффекты прекрасны.

Ответ 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()

Ответ 3

Вы можете обернуть его в вычислительное выражение, чтобы оно выглядело чистым. Вы даже можете расширить его с помощью кода для обработки тайм-аутов и сбитых серверов. Я новичок в концепции, поэтому, если бы эксперты могли меня обучать, если что-то кажется неуместным.

Я думаю, что эта концепция была бы гораздо более полезной, если бы вы пронизывали больше, чем просто репозиторий, но я хотел, чтобы это было просто.

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