2015-09-05 22 views
8

Sto cercando di creare una soluzione che abbia una libreria di livello inferiore che sappia che è necessario salvare e caricare i dati quando vengono chiamati determinati comandi, ma l'implementazione delle funzioni di salvataggio e caricamento verrà fornita in una piattaforma- progetto specifico che fa riferimento alla libreria di livello inferiore.Programmazione funzionale e inversione di dipendenza: come astrarre lo spazio di archiviazione?

Ho alcuni modelli, come ad esempio:

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

type Situation = { SituationID: SituationID } 

E quello che voglio fare è essere in grado di definire e funzioni come la chiamata:

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

Esiste un modo per definire questa pulito nell'idioma funzionale, preferibilmente evitando lo stato mutabile (che non dovrebbe essere comunque necessario)?

Un modo per farlo potrebbe essere simile a questo:

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) 

E ci sono variazioni su questo, ma tutti quelli che mi vengono in mente comporta un qualche tipo di stato non inizializzato. (In Xamarin, esiste anche DependencyService, ma è anch'esso una dipendenza che vorrei evitare.)

C'è un modo per scrivere codice che chiama una funzione di archiviazione, che non è stata ancora implementata, e quindi implementare esso, SENZA usare lo stato mutabile?

(Nota: questa domanda non si tratta di stoccaggio in quanto tale - questo è solo l'esempio che sto usando Si tratta di come iniettare funzioni senza l'utilizzo di stato mutevole inutili..)

risposta

15

altre risposte qui sarà forse educare su come implementare la monade IO in F #, che è certamente un'opzione. In F #, però, spesso compio le funzioni di comporre con altre funzioni. Non è necessario definire una 'interfaccia' o un tipo particolare per fare ciò.

Sviluppa il tuo sistema da Esterno e definisci le funzioni di alto livello concentrandoti sul comportamento che devono implementare. Rendili funzioni di ordine superiore passando le dipendenze come argomenti.

È necessario interrogare un archivio dati? Passa a un argomento loadUser. Hai bisogno di salvare l'utente? Passare un argomento saveUser:

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

L'argomento loadUser viene dedotta essere di tipo User -> User option e saveUser come User -> unit, perché doSomethingInterestingWith è una funzione del tipo User -> User.

È ora possibile 'implementare' loadUser e saveUser scrivendo funzioni che chiamano nella libreria di livello inferiore.

La reazione tipica che ottengo a questo approccio è: Questo mi richiederà di inoltrare troppi argomenti alla mia funzione!

In effetti, se ciò accade, considerare se questo non è un odore che la funzione sta tentando di fare troppo.

Poiché il Dependency Inversion Principle è menzionato nel titolo di questa domanda, vorrei sottolineare che il SOLID principles funziona meglio se tutti sono applicati in concerto. Il Interface Segregation Principle dice che le interfacce dovrebbero essere il più piccole possibile, e non le riduci più piccole di quando ogni 'interfaccia' è una singola funzione.

Per un articolo più dettagliato che descrive questa tecnica, è possibile leggere il mio Type-Driven Development article.

1

È possibile astrarre lo storage dietro all'interfaccia IStorage. Penso che sia stata la tua intenzione.

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 un'altra parte del programma è possibile avere più implementazioni di archiviazione.

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

E dopo avete tutti i vostri tipi definiti è possibile decidere quale utilizzare.

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

let user = storageSystem.LoadUser userID