2011-09-20 11 views
5

Sto passando gradualmente a F # per molti dei miei progetti a casa, ma sono un po 'perplesso su come collegare insieme applicazioni complete e, in particolare, preoccupazioni trasversali.Registrazione/repository della struttura dell'applicazione F #

In C# se voglio registrare roba, userei l'injection dependance per passare un ILogger in ogni classe, e quindi questo può essere chiamato bello e facilmente dal codice. Posso verificare nei miei test che, data una particolare situazione, i registri vengano scritti, passando una finta e verificandoli.

public class MyClass 
{ 
    readonly ILogger _logger; 
    public MyClass(ILogger logger) 
    { 
     _logger = logger; 
    } 

    public int Divide(int x, int y) 
    { 
     if(y == 0) 
     { 
      _logger.Warn("y was 0"); 
      return 0; 
     } 
     return x/y; 
    } 
} 

In F # Sto utilizzando i moduli molto di più, in modo che il sopra diventerebbe

module Stuff 

let divde x y = 
    match y with 
    | 0 -> 0 
    | _ -> x/y 

Ora, se ho avuto un modulo chiamato registrazione ho potuto solo aprire questo e utilizzare una funzione di log da lì nel caso in cui y sia 0, ma come dovrei iniettarlo per il test dell'unità?

Potrei avere ciascuna funzione prendere una funzione di registro (stringa -> unità) e quindi utilizzare un'applicazione parziale per collegarli, ma sembra un sacco di lavoro, come se creerebbe una nuova funzione che avvolge la chiamata effettiva all'interno una chiamata di registrazione. C'è uno schema particolare o un po 'di F # che mi manca che può farlo? (Ho visto la funzione kprintf ma ancora non so come specificare la funzione per vari scenari di test, mentre utilizzo un'implementazione concreta per l'applicazione completa)

Analogamente, come si esegue lo stub di un repository quei dati recuperati? Avete bisogno di avere qualche classe istanziata e impostare le funzioni CRUD su di esso, o c'è un modo per iniettare i moduli che si aprono (a parte #define)

+0

http://stackoverflow.com/questions/2003487/architectural-thinking-in-functional-languages ​​http://stackoverflow.com/questions/192090/how-do-you-design-a-functional-program –

risposta

2

Questa è una risposta di base. In primo luogo, sembra che tu stia pensando a classi e moduli come intercambiabili. Le classi incapsulano i dati e in questo senso sono più analoghi ai record e ai DU. I moduli, d'altra parte, incapsulano le funzionalità (sono compilate in classi statiche). Quindi, penso che tu abbia già menzionato le tue opzioni: applicazione di funzioni parziali, passaggio delle funzioni come dati, o ... iniezione di dipendenza. Per il tuo caso particolare sembra più facile mantenere quello che hai.

Un'alternativa utilizza le direttive del preprocessore per includere diversi moduli.

#if TESTING 
open LogA 
#else 
open LogB 
#endif 

DI non è necessariamente inadeguato in un linguaggio funzionale. Vale la pena sottolineare che F # rende la definizione e l'adempimento delle interfacce ancora più semplice di, ad esempio, C#.

+0

hmmm, quindi immagina che F # non aveva classi solo record e DU, quindi il logging doveva essere inserito nei moduli come di solito si vuole registrare su un'operazione (almeno nel debug) piuttosto che sul risultato di un'operazione. Ciò significherebbe che DI è fuori dalla finestra, quindi l'applicazione parziale è tutto ciò che rimane. Puoi consigliare qualche lettura sull'architettura funzionale per caso? Dato che è stato intorno così a lungo sono sicuro che questo genere di cose è già stato risolto, ma tutti i libri che ho visto si concentrano sul "piccolo" piuttosto che sul "grande" – Dylan

+0

che vorrei averlo fatto. Ricordo di aver lottato anche con questo all'inizio. Mi aspettavo che l'architettura "grande" fosse radicalmente diversa in F #. Alcuni linguaggi funzionali consentono di parametrizzare i moduli. Sfortunatamente, F # no. OO è ancora un buon approccio "nel grande". So che la domanda è stata posta alcune volte su SO. È stata visualizzata una ricerca rapida: http://stackoverflow.com/questions/2003487/architectural-thinking-in-functional-languages. Ci possono essere altri – Daniel

+0

Grazie per il collegamento, ho effettuato una ricerca in precedenza ma non ho trovato nulla che in realtà fornisse indicazioni. Penso che tu abbia ragione con l'applicazione parziale, suppongo che debba solo salire di un livello di incapsulamento comunque prima che le dipendenze inizino a essere cablate dall'applicazione di hosting. – Dylan

3

Se non è necessario modificare il logger in fase di runtime, quindi utilizzare una direttiva del compilatore o #if per scegliere tra due implementazioni di un logger (come suggerito da Daniel) è probabilmente il modo migliore e più semplice.

Dal punto di vista funzionale, l'iniezione delle dipendenze equivale alla parametrizzazione di tutto il codice tramite una funzione di registrazione. La cosa brutta è che è necessario propagare la funzione ovunque (e ciò rende il codice un po 'disordinato). Puoi anche solo creare una variabile mutabile globale in un modulo e impostarla su un'istanza di qualche interfaccia ILogger - Penso che questa sia una soluzione abbastanza accettabile per F #, perché devi modificare questa variabile solo in un paio di punti.

Un'altra alternativa (più "pura") sarebbe quella di definire un flusso di lavoro (aka monad) per la registrazione. Questa è una buona scelta solo se scrivi tutto il tuo codice in F #. Questo esempio è discusso nel capitolo 12 del mio libro, che è available as a free sample.Quindi puoi scrivere qualcosa del tipo:

let write(s) = log { 
    do! logMessage("writing: " + s) 
    Console.Write(s) } 

let read() = log { 
    do! logMessage("reading") 
    return Console.ReadLine() } 

let testIt() = log { 
    do! logMessage("starting") 
    do! write("Enter name: ") 
    let! name = read() 
    return "Hello " + name + "!" } 

La cosa buona di questo approccio è che è una bella tecnica funzionale. Il codice di prova dovrebbe essere semplice, in quanto una funzione che restituisce Log<'TResult> fornisce un valore e un record dei suoi effetti collaterali, quindi è sufficiente confrontare i risultati! Tuttavia, potrebbe essere eccessivo, perché dovresti completare ogni calcolo che utilizza l'accesso nel blocco log { .. }.

+0

Grazie Tomas, Sono ancora in attesa che la mutabilità sia malvagia. Nel mio codice C# creo sempre le dipendenze di sola lettura. Se creo un logger mutabile nel codice, suppongo che non ci sia un modo semplice per proteggerlo dalla riassegnazione accidentale. Suppongo, come ho detto in un altro commento, che i livelli superiori dell'applicazione (funzionalità derivata) non accedano comunque ai moduli inferiori. – Dylan

+0

@Dylan - Sì, la dichiarazione dei moduli è sensibile agli ordini, quindi il modulo di registrazione dovrebbe essere il primo. È vero che la mutabilità è cattiva (in F #) in generale. Tuttavia, se non è necessario riassemblare il registratore, allora è solo l'opzione più semplice. –

0

Ecco un'implementazione del vostro codice C# in F #, che è simile a quello iniziale C#

module stuff 

type MyClass(logger:#Ilogger) = 
    member x.Divide a b = 
     if b=0 then 
      logger.Warn("y was 0") 
      0 
     else 
      x/y 

questo dovrebbe consentire di utilizzare le stesse tecniche che si conoscono da C# per la vostra registrazione

+0

grazie, ma preferisco usare C# per questo tipo di approccio. Non ho davvero chiarito, ma era più per come avresti fatto questo approccio usando solo record e definizioni di funzioni senza classi – Dylan

+0

Un minore no - non hai bisogno del tipo '# ILogger' in questo caso, perché F # inserisce automaticamente il cast quando si chiama un metodo o un costruttore. Usare il tipo di hash qui sembra un po 'strano - suppongo che renda persino l'intera classe generica (parametrizzata sul tipo di logger), che qui non è necessaria. –

+0

@Tomas - questo è quello che succede quando non mi preoccupo di usare il compilatore per controllare il mio codice –

Problemi correlati