Functional programming and dependency inversion: how to abstract storage?

社会主义新天地 提交于 2019-12-08 18:33:42

问题


I'm trying to create a solution that has a lower-level library that will know that it needs to save and load data when certain commands are called, but the implementation of the save and load functions will be provided in a platform-specific project which references the lower-level library.

I have some models, such as:

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

type Situation = { SituationID: SituationID }

And what I want to do is be able to define and call functions such as:

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

Is there any way to define this cleanly in the functional idiom, preferably while avoiding mutable state (which shouldn't be necessary anyway)?

One way to do it might look something like this:

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)

And there are variations on this, but all the ones I can think of involve some kind of uninitialized state. (In Xamarin, there's also DependencyService, but that is itself a dependency I would like to avoid.)

Is there any way to write code that calls a storage function, which hasn't been implemented yet, and then implement it, WITHOUT using mutable state?

(Note: this question is not about storage itself -- that's just the example I'm using. It's about how to inject functions without using unnecessary mutable state.)


回答1:


Other answers here will perhaps educate you on how to implement the IO monad in F#, which is certainly an option. In F#, though, I'd often just compose functions with other functions. You don't have to define an 'interface' or any particular type in order to do this.

Develop your system from the Outside-In, and define your high-level functions by focusing on the behaviour they need to implement. Make them higher-order functions by passing in dependencies as arguments.

Need to query a data store? Pass in a loadUser argument. Need to save the user? Pass in a saveUser argument:

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

The loadUser argument is inferred to be of type User -> User option, and saveUser as User -> unit, because doSomethingInterestingWith is a function of type User -> User.

You can now 'implement' loadUser and saveUser by writing functions that call into the lower-level library.

The typical reaction I get to this approach is: That'll require me to pass in too many arguments to my function!

Indeed, if that happens, consider if that isn't a smell that the function is attempting to do too much.

Since the Dependency Inversion Principle is mentioned in the title of this question, I'd like to point out that the SOLID principles work best if all of them are applied in concert. The Interface Segregation Principle says that interfaces should be as small as possible, and you don't get them smaller than when each 'interface' is a single function.

For a more detailed article describing this technique, you can read my Type-Driven Development article.




回答2:


You can abstract storage behind interface IStorage. I think that was your intention.

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

In another part of your program you can have multiple storage implementations.

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

And after you have all your types defined you can decide which to use.

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

let user = storageSystem.LoadUser userID


来源:https://stackoverflow.com/questions/32411667/functional-programming-and-dependency-inversion-how-to-abstract-storage

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!