问题
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