2015-09-05 8 views
8

特定のコマンドが呼び出されたときにデータを保存してロードする必要があることを知る下位レベルのライブラリを持つソリューションを作成しようとしていますが、保存およびロード機能の実装は、下位レベルのライブラリを参照する特定のプロジェクトです。これを定義する方法を関数型プログラミングと依存関係の逆転:記憶域を抽象化する方法

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

あり:

type User = { UserID: UserID 
       Situations: SituationID list } 

type Situation = { SituationID: SituationID } 

そして、何私がやりたいことを定義し、のような関数を呼び出すことができます:

私は、次のようないくつかのモデルを持っています機能的なイディオムをきれいに、できれば変更可能な状態を避けながら(これは必ずしも必要ではない)?

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もありますが、それはそれ自体避けたい依存関係です)。

まだ実装されていない記憶機能を呼び出すコードを書く方法はありますかそれは、可変状態を使用せずに?

(注:この問題は、ストレージ自体に関するものではありません - それは私が使用している例でしかありませんそれは不必要な可変状態を使用せずに機能を注入する方法についてです。。)

答えて

15

他の回答をここにおそらくあなたを教育しますF#でIOモナドを実装する方法については、確かにオプションです。しかし、F#では、私はしばしばは、他の関数と関数を構成します。これを行うには、「インターフェース」や特定のタイプを定義する必要はありません。

Outside-Inからシステムを開発し、実装する必要がある動作に焦点を当てて高度な機能を定義します。依存関係を引数として渡して、上位関数にしてください。

データストアを照会する必要がありますか? loadUser引数を渡します。ユーザーを保存する必要がありますか? saveUser引数に渡します。doSomethingInterestingWithはタイプUser -> Userの関数であるので、

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、およびUser -> unitなどsaveUserであると推測されます。

loadUsersaveUserは、下位レベルのライブラリに呼び出す関数を書くことで '実装'できるようになりました。

私がこのアプローチに到達する典型的な反応は次のとおりです。それは私の関数に多すぎる引数を渡す必要があります!

実際、そのようなことが起こった場合、その機能があまりにも多くのことを試みている匂いではないかどうかを検討してください。

この質問のタイトルにDependency Inversion Principleが記載されているので、SOLID principlesはすべてが一括して適用されると最も効果的であることを指摘したいと思います。 Interface Segregation Principleでは、インターフェイスはできるだけ小さくする必要があり、それぞれのインターフェイスが単一の関数である場合よりも小さくする必要はありません。

このテクニックの詳細な記事は、Type-Driven Development articleです。

1

インターフェイス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 
関連する問題