2015-09-05 7 views
8

Ich versuche, eine Lösung zu erstellen, die eine untergeordnete Bibliothek hat, die weiß, dass sie Daten speichern und laden muss, wenn bestimmte Befehle aufgerufen werden. Die Implementierung der Speicher- und Ladefunktionen wird jedoch in einer Plattform zur Verfügung gestellt. spezifisches Projekt, das auf die untergeordnete Bibliothek verweist.Funktionale Programmierung und Abhängigkeitsinversion: Wie kann man den Speicher abstrahieren?

Ich habe einige Modelle, wie zum Beispiel:

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

type Situation = { SituationID: SituationID } 

Und was will ich in der Lage zu tun sein, um Funktionen zu definieren und rufen wie:

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

Gibt es eine Möglichkeit, dies zu definieren sauber in der funktionalen Sprache, möglichst unter Vermeidung eines veränderlichen Zustands (der sowieso nicht notwendig sein sollte)?

Eine Möglichkeit, es zu tun, so etwas wie dies aussehen könnte:

auf diesem
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) 

Und es gibt Variationen, aber alle diejenigen, die ich von beinhalten eine Art von nicht initialisierten Zustand denken kann. (In Xamarin gibt es auch DependencyService, aber das ist selbst eine Abhängigkeit, die ich gerne vermeiden würde.)

Gibt es eine Möglichkeit, Code zu schreiben, der eine Speicherfunktion aufruft, die noch nicht implementiert wurde, und dann implementieren es, OHNE einen veränderlichen Zustand zu verwenden?

(Hinweis: Diese Frage ist nicht über Lagerung selbst - das ist nur das Beispiel ich benutze Es geht darum, wie Funktionen zu injizieren, ohne unnötigen wandelbaren Zustand zu verwenden..)

Antwort

15

Andere Antworten hier wird vielleicht Sie erziehen wie man die IO-Monade in F # implementiert, was sicherlich eine Option ist. In F # habe ich oft nur Funktionen mit anderen Funktionen komponieren. Sie müssen dazu keine 'Schnittstelle' oder einen bestimmten Typ definieren.

Entwickeln Sie Ihr System von der Outside-In, und definieren Sie Ihre High-Level-Funktionen, indem Sie sich auf das Verhalten konzentrieren, das sie implementieren müssen. Machen Sie sie Funktionen höherer Ordnung indem Sie Abhängigkeiten als Argumente übergeben.

Sie müssen einen Datenspeicher abfragen? Pass in einem loadUser Argument. Müssen Sie den Benutzer speichern? Der Pass in einem saveUser Argumente:

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

loadUser Das Argument des Typs seine User -> User option abgeleitet wird, und als saveUserUser -> unit, weil doSomethingInterestingWith eine Funktion vom Typ User -> User.

Sie können jetzt loadUser und saveUser "implementieren", indem Sie Funktionen schreiben, die in die Bibliothek der unteren Ebene aufrufen.

Die typische Reaktion, die ich auf diesem Ansatz erhalten ist: Das wird mir verlangen zu viele Argumente meiner Funktion zu übergeben!

In der Tat, wenn das passiert, bedenken Sie, wenn das nicht ein Geruch ist, dass die Funktion versucht, zu viel zu tun.

Da die Dependency Inversion Principle im Titel dieser Frage erwähnt wird, möchte ich darauf hinweisen, dass die SOLID principles am besten funktionieren, wenn alle von ihnen im Konzert angewendet werden. Die Interface Segregation Principle sagt, dass Schnittstellen so klein wie möglich sein sollten, und Sie nicht kleiner als wenn jede "Schnittstelle" eine einzige Funktion ist.

Für einen ausführlicheren Artikel, der diese Technik beschreibt, können Sie meine Type-Driven Development article lesen.

1

Sie können den Speicher hinter der Schnittstelle IStorage abstrahieren. Ich denke, das war deine Absicht.

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 einem anderen Teil des Programms können Sie mehrere Speicherimplementierungen haben.

type MyStorage() = 
    interface IStorage with 
     member x.LoadUser uid -> ... 
     member x.SaveUser u -> ... 

Und nachdem Sie alle Ihre Typen definiert haben, können Sie entscheiden, welche zu verwenden.

let storageSystem = 
    if today.IsShinyDay 
    then MyStorage() :> IStorage 
    else Storage.noStorage 

let user = storageSystem.LoadUser userID