Шаблон репозитория в 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 })
}