函数式编程和依赖倒置:如何抽象存储



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

我有一些模型,例如:

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

是否有任何方法来编写代码调用存储函数,这还没有实现,然后实现它,不使用可变状态?

(注意:这个问题不是关于存储本身的——这只是我使用的例子。它是关于如何注入函数而不使用不必要的可变状态。

这里的其他答案可能会教你如何在f#中实现IO单子,这当然是一种选择。但是,在f#中,我通常只是将函数与其他函数组合在一起。你不需要定义一个"接口"或任何特定的类型来做到这一点。

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

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

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

由于doSomethingInterestingWithUser -> User类型的函数,因此推断loadUser参数为User -> User option类型,saveUserUser -> unit类型。

你现在可以通过编写调用底层库的函数来"实现"loadUsersaveUser

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

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

既然这个问题的标题中提到了依赖倒置原则,我想指出的是,如果所有的SOLID原则都被一致应用,那么它们的工作效果最好。接口隔离原则说接口应该尽可能小,当每个"接口"是一个单一的功能时,你不能让它们更小。

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

可以在接口storeage后面抽象存储。我想那就是你的意图。

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

最新更新