【问题标题】:Functional programming and dependency inversion: how to abstract storage?函数式编程和依赖倒置:如何抽象存储?
【发布时间】:2015-09-05 09:47:15
【问题描述】:

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

我有一些模型,例如:

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,但这本身就是我想避免的依赖项。)

有没有什么方法可以编写调用尚未实现的存储函数的代码,然后在不使用可变状态的情况下实现它?

(注意:这个问题与存储本身无关——这只是我正在使用的示例。它是关于如何在不使用不必要的可变状态的情况下注入函数。)

【问题讨论】:

    标签: xamarin f# functional-programming storage abstraction


    【解决方案1】:

    这里的其他答案可能会教您如何在 F# 中实现 IO monad,这当然是一种选择。不过,在 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 类型,saveUserUser -> unit,因为doSomethingInterestingWithUser -> User 类型的函数。

    您现在可以通过编写调用低级库的函数来“实现”loadUsersaveUser

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

    确实,如果发生这种情况,请考虑这是否不是函数试图做太多事情的气味。

    由于这个问题的标题中提到了Dependency Inversion Principle,我想指出,如果所有这些都一起应用,SOLID principles 效果最好。 Interface Segregation Principle 表示接口应该尽可能小,并且不会比每个“接口”都是单个函数时更小。

    有关描述此技术的更详细文章,您可以阅读我的Type-Driven Development article

    【讨论】:

      【解决方案2】:

      您可以在接口 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
      

      【讨论】:

        猜你喜欢
        • 2017-07-17
        • 1970-01-01
        • 2014-10-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-10-04
        • 2022-11-20
        • 1970-01-01
        相关资源
        最近更新 更多