2013-08-27 17 views
12

Data un'applicazione ASP.NET MVC con i seguenti livelli:Quando utilizzare le interfacce e quando utilizzare le funzioni di ordine superiore?

  • UI (Viste, CSS, JavaScript, ecc)
  • Controllers
  • Servizi (Contiene la logica di business, e l'accesso ai dati)

Il motivo per cui non esiste un livello di accesso ai dati separato, è che sto utilizzando provider di tipo SQL.

(Il seguente codice potrebbe non funzionare, in quanto è solo una bozza non elaborata). Ora immaginate un servizio denominato UserService definito come:

module UserService = 
    let getAll memoize f = 
     memoize(fun _ -> f) 

    let tryGetByID id f memoize = 
     memoize(fun _ -> f id) 

    let add evict f name keyToEvict = 
     let result = f name 
     evict keyToEvict 
     result 

E poi nel mio strato di Controller, avrò un altro modulo chiamato UserImpl o potrebbe benissimo essere chiamato UserMemCache:

module UserImpl = 
    let keyFor = MemCache.keyFor 
    let inline memoize args = 
     MemCache.keyForCurrent args 
     |> CacheHelpers.memoize0 MemCache.tryGet MemCache.store 

    let getAll = memoize [] |> UserService.getAll 
    let tryGetByID id = memoize [id] |> UserService.tryGetByID id 
    let add = 
     keyFor <@ getAll @> [id] 
     |> UserService.add MemCache.evict 

Il l'uso di questo sarebbe come:

type UserController() = 
    inherit Controller() 

    let ctx = dbSchema.GetDataContext() 

    member x.GetAll() = UserImpl.getAll ctx.Users 
    member x.UserNumberOne = UserImpl.tryGetByID ctx.Users 1 
    member x.UserNumberTwo = UserImpl.tryGetByID ctx.Users 2 
    member x.Add(name) = UserImpl.add ctx.Users name 

Utilizzando le interfacce, avremmo la seguente implementazione:

type UserService(ICacheProvider cacheProvider, ITable<User> db) = 
    member x.GetAll() = 
     cacheProvider.memoize(fun _ -> db |> List.ofSeq) 

    member x.TryGetByID id = 
     cacheProvider.memoize(fun _ -> db |> Query.tryFirst <@ fun z -> z.ID = ID @>) 

    member x.Add name = 
     let result = db.Add name 
     cacheProvider.evict <@ x.GetAll() @> [] 
     result 

E l'uso sarebbe qualcosa di simile:

type UserController(ICacheProvider cacheProvider) = 
    inherit Controller() 

    let ctx = dbSchema.GetDataContext() 
    let userService = new UserService(cacheProvider, ctx.Users) 

    member x.GetAll() = userService.GetAll() 
    member x.UserNumberOne = userService.TryGetByID 1 
    member x.UserNumberTwo = userService.TryGetByID 2 

Ovviamente l'implementazione dell'interfaccia ha molto meno codice, ma in realtà non voglia codice funzionale più. Se inizio a utilizzare le interfacce in tutta la mia app Web, quando so quando utilizzare le funzioni di ordine superiore? - altrimenti finirò con una semplice vecchia soluzione OOP.

Quindi in breve: quando devono essere utilizzate le interfacce e quando utilizzare le funzioni di ordine superiore? - deve essere tracciata una linea, o saranno tutti i tipi e le interfacce, per cui la bellezza di FP scomparirà.

+1

Ho scritto alcune riflessioni su questo sul mio blog, cercando di chiarire la relazione tra gli oggetti e le funzioni di ordine superiore tra le altre cose: http://bugsquash.blogspot.com/2013/08/objects-and-functional-programming.html –

risposta

10

Su interfacce. Prima di tutto, penso che puoi vedere le interfacce come coppie di funzioni appena nominate. Se si dispone di:

type ICacheProvider = 
    abstract Get : string -> option<obj> 
    abstract Set : string * obj -> unit 

allora questo è più o meno equivalente ad avere una coppia (o di un record) di funzioni:

type CacheProvider = (string -> option<obj>) * (string * obj -> unit) 

Il vantaggio di utilizzare interfacce è che si dà il tipo di un nome (lo otterresti anche con i record) e stai esprimendo più chiaramente la tua intenzione (altri componenti possono implementare l'interfaccia).

Penso che l'utilizzo di un'interfaccia sia una buona idea se si hanno più di 2 funzioni che vengono passate spesso ad altre funzioni insieme: in tal modo si evita di avere troppi parametri.

Modulo o classe. La vera differenza nel codice è se utilizzare il modulo con funzioni di ordine superiore o una classe che utilizza l'interfaccia come argomento del costruttore. F # è un linguaggio multi-paradigma che combina lo stile funzionale e OO, quindi penso che l'uso delle classi in questo modo sia perfetto.(È comunque possibile trarre vantaggio dallo stile funzionale quando si definiscono tipi di dati per rappresentare il dominio ecc.)

Una cosa da tenere a mente è che la programmazione funzionale è tutta relativa alla composizione . Questo potrebbe non essere utile in questo caso, ma preferisco sempre scrivere codice che posso comporre per aggiungere più funzionalità piuttosto che codice che mi richiede di fornire qualcosa quando voglio usarlo.

Forse si potrebbe scrivere in modo che il codice di accesso al database non fa direttamente la memorizzazione nella cache (questo dovrebbe includere tutte le query di database e la logica pre-processing):

module UserService = 
    let getAll() = (...) 
    let tryGetByID id = (...) 
    let add name = (...) 

... e poi definire un tipo questo lo avvolge e aggiunge il caching (e questo sarebbe poi usato dal tipo principale dell'applicazione web - è abbastanza simile al tipo definito nel tuo esempio, ma ora stiamo separando l'accesso al database e la memorizzazione usando un provider di cache) :

type UserService(cacheProvider:ICacheProvider) = 
    member x.GetAll() = cacheProvider.memoize UserSerivce.getAll() 
    member x.TryGetByID id = cacheProvider.memoize UserService.tryGetByID id 
    member x.Add name = cacheProvider.memoize UserService.add name 

Riepilogo. Ma - Penso che il tuo approccio usando una classe che prende il numero ICacheProvider sia perfettamente a posto - F # è abbastanza buono nel mixare stile funzionale e orientato agli oggetti. L'esempio che ho postato è in realtà solo una possibile estensione che potrebbe essere utile in progetti più grandi (se si desidera utilizzare aspetti funzionali e separare chiaramente diversi aspetti della funzionalità)

+0

Anche i framework di testing delle unità tendono a giocare molto meglio con le interfacce di derisione piuttosto che prendere in giro es tuple di firme di funzioni. –

Problemi correlati