函数式编程和依赖倒置:如何抽象存储?
Functional programming and dependency inversion: how to abstract storage?
我正在尝试创建一个具有较低级别库的解决方案,该库将知道在调用某些命令时需要保存和加载数据,但保存和加载功能的实现将在引用较低级别库的特定于平台的项目。
我有一些模型,比如:
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 monad,这当然是一个选项。不过,在 F# 中,我通常 将函数与其他函数组合 。您不必为了执行此操作而定义 'interface' 或任何特定类型。
从 由外向内 开发您的系统,并通过关注它们需要实现的行为来定义您的高级功能。通过传入依赖项作为参数使它们成为 高阶函数。
需要查询数据存储?传入一个 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
是[=]类型的函数18=].
您现在可以 'implement' loadUser
和 saveUser
通过编写调用较低级别库的函数。
我对这种方法的典型反应是:这需要我向我的函数传递太多参数!
确实,如果发生这种情况,请考虑一下这是否是函数试图做太多事情的味道。
因为 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 说接口应该尽可能小,而且你不会让它们比每个 'interface' 是单个函数时更小。
有关描述此技术的更详细的文章,您可以阅读我的 Type-Driven Development article。
您可以在 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
我正在尝试创建一个具有较低级别库的解决方案,该库将知道在调用某些命令时需要保存和加载数据,但保存和加载功能的实现将在引用较低级别库的特定于平台的项目。
我有一些模型,比如:
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 monad,这当然是一个选项。不过,在 F# 中,我通常 将函数与其他函数组合 。您不必为了执行此操作而定义 'interface' 或任何特定类型。
从 由外向内 开发您的系统,并通过关注它们需要实现的行为来定义您的高级功能。通过传入依赖项作为参数使它们成为 高阶函数。
需要查询数据存储?传入一个 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
是[=]类型的函数18=].
您现在可以 'implement' loadUser
和 saveUser
通过编写调用较低级别库的函数。
我对这种方法的典型反应是:这需要我向我的函数传递太多参数!
确实,如果发生这种情况,请考虑一下这是否是函数试图做太多事情的味道。
因为 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 说接口应该尽可能小,而且你不会让它们比每个 'interface' 是单个函数时更小。
有关描述此技术的更详细的文章,您可以阅读我的 Type-Driven Development article。
您可以在 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