Функциональное программирование и инверсия зависимостей: как абстрагироваться от хранилища?

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

У меня есть несколько моделей, например:

type User = { UserID: UserID
              Situations: SituationID list }

type Situation = { SituationID: SituationID }

И то, что я хочу сделать, это иметь возможность определять и вызывать такие функции, как:

do saveUser ()
let user = loadUser (UserID 57)

Есть ли способ четко определить это в функциональной идиоме, желательно, избегая изменяемого состояния (которое в любом случае не должно быть необходимым)?

Один из способов сделать это может выглядеть примерно так:

type IStorage = {
    saveUser: User->unit;
    loadUser: UserID->User }

module Storage =
    // initialize save/load functions to "not yet implemented"
    let mutable storage = {
        saveUser = failwith "nyi";
        loadUser = failwith "nyi" }

// ....elsewhere:
do Storage.storage = { a real implementation of IStorage }
do Storage.storage.saveUser ()
let user = Storage.storage.loadUser (UserID 57)

И на этот счет есть вариации, но все, что я могу придумать, связаны с каким-то неинициализированным состоянием. (В Xamarin также есть DependencyService, но я хотел бы избежать этой зависимости.)

Есть ли способ написать код, который вызывает функцию хранения, которая еще не реализована, а затем реализовать ее БЕЗ использования изменяемого состояния?

(Примечание: этот вопрос не о самом хранилище — это просто пример, который я использую. Он о том, как внедрять функции без использования ненужного изменяемого состояния.)


person Overlord Zurg    schedule 05.09.2015    source источник


Ответы (2)


Другие ответы здесь, возможно, расскажут вам, как реализовать монаду ввода-вывода в F #, что, безусловно, является вариантом. Однако в F# я часто просто составлял функции с другими функциями. Вам не нужно определять «интерфейс» или какой-либо конкретный тип, чтобы сделать это.

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

Нужно запросить хранилище данных? Передайте аргумент loadUser. Нужно спасти пользователя? Передайте аргумент saveUser:

let myHighLevelFunction loadUser saveUser (userId) =
    let user = loadUser (UserId userId)
    match user with
    | Some u ->
        let u' = doSomethingInterestingWith u
        saveUser u'
    | None -> ()

Предполагается, что аргумент loadUser относится к типу User -> User option, а saveUser к типу User -> unit, поскольку doSomethingInterestingWith является функцией типа User -> User.

Теперь вы можете «реализовать» loadUser и saveUser, написав функции, которые вызывают библиотеку более низкого уровня.

Типичная реакция на такой подход: Это потребует от меня передачи слишком большого количества аргументов в мою функцию!

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

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

Более подробную статью, описывающую этот метод, вы можете прочитать в моем разработке, управляемой типами. статья.

person Mark Seemann    schedule 05.09.2015

Вы можете абстрагировать хранилище за интерфейсом IStorage. Я думаю, это было вашим намерением.

type IStorage =
    abstract member LoadUser : UserID -> User
    abstract member SaveUser : User -> unit

module Storage =
    let noStorage = 
        { new IStorage with
             member x.LoadUser _ -> failwith "not implemented"
             member x.SaveUser _ -> failwith "not implemented"
        }

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

type MyStorage() =
    interface IStorage with
        member x.LoadUser uid -> ...
        member x.SaveUser u   -> ...

И после того, как вы определили все свои типы, вы можете решить, какой из них использовать.

let storageSystem =
    if today.IsShinyDay
    then MyStorage() :> IStorage
    else Storage.noStorage

let user = storageSystem.LoadUser userID
person Bartek Kobyłecki    schedule 05.09.2015