函数式编程和依赖倒置:如何抽象存储? [英] Functional programming and dependency inversion: how to abstract storage?

查看:176
本文介绍了函数式编程和依赖倒置:如何抽象存储?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我试图创建一个具有较低级库的解决方案,在知道调用某些命令时它需要保存和加载数据,但保存和加载函数的实现将在平台中提供 - 特定的项目,它引用了较低级别的库。



我有一些模型,比如:

  type User = {UserID:UserID 
情境ID:情境ID列表}

类型情境= {情境ID:情境ID}

我想要做的是能够定义和调用函数,如:

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

有没有什么办法可以在函数式语言中干净地定义它,最好避免可变状态(这不应该是必须的)?

做到这一点可能看起来像这样:

  type IStorage = {
saveUser :用户 - >单元;
loadUser:UserID->用户}

模块存储=
//初始化保存/加载函数为尚未实现
让可变存储= {
saveUser = failwithnyi;
loadUser = failwithnyi}

// ....其他地方:
Storage.storage = {IStorage的真正实现}
做存储。 storage.saveUser()
let user = Storage.storage.loadUser(UserID 57)

在这方面还有一些变化,但我能想到的所有变化都涉及某种未初始化的状态。 (在Xamarin中,还有DependencyService,但这本身就是我想避免的依赖项)。

是否有任何方法可以编写调用存储的代码函数,它尚未实现,然后实现它,而不使用可变状态?



(注意:这个问题不是关于存储本身 - - 这只是我使用的例子,它是关于如何在不使用不必要的可变状态的情况下注入函数。) 解决方案

其他答案这里可能会教你如何在F#中实现IO monad,这当然是一种选择。不过,在F#中,我经常只是用其他函数组合函数。您无需为此定义接口或任何特定类型。



Outside-In ,并通过关注他们需要实施的行为来定义您的高级功能。通过传递依赖关系作为参数来使它们成为高阶函数



需要查询数据存储?传入 loadUser 参数。需要保存用户吗?传入 saveUser 参数:

  let myHighLevelFunction loadUser saveUser(userId) = 
let user = loadUser(UserId userId)
将用户与
匹配|一些u - >
let u'= doSomethingInterestingWith u
saveUser u'
|无 - > ()

loadUser 参数被推断为类型为用户 - >用户选项 saveUser 作为用户 - >单元,因为 doSomethingInterestingWith 是类型用户 - >的函数。用户



您现在可以'执行' loadUser saveUser 通过编写调用低级库的函数。



我对这种方法的典型反应是:需要我将太多参数传递给我的函数!



确实,如果发生这种情况,请考虑一下,如果这不是一种气味试图做得太多。



由于依赖项Inversion Principle 在这个问题的标题中提到,我想指出 SOLID原则如果所有这些原则都一致应用,则效果最佳。 界面隔离原则说接口应该尽可能小,而且你不能接收它们小于当每个接口是单个函数时。



有关描述此技术的更详细的文章,您可以阅读我的类型驱动开发文章


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

解决方案

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.

这篇关于函数式编程和依赖倒置:如何抽象存储?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆