2013-08-27 22 views
12

Biorąc aplikacji ASP.NET MVC z następujących warstw:Kiedy korzystać z interfejsów i kiedy korzystać z funkcji wyższego rzędu?

  • UI (widoki, CSS, JavaScript, itp)
  • Sterowniki
  • usług (Zawiera logiki biznesowej i dostępu do danych)

Powodem braku oddzielnej warstwy dostępu do danych jest to, że korzystam z dostawcy typu SQL.

(Poniższy kod może nie działać, ponieważ jest to tylko wersja robocza). Teraz wyobraź sobie usługę o nazwie UserService zdefiniowane następująco:

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 

a potem w moim warstwy kontrolerów, będę mieć inny moduł nazwany UserImpl lub może to być równie dobrze nazwana 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 

użycie tego byłoby jak:

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 

Korzystanie z interfejsów, mielibyśmy następujące wdrożenie:

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 

I wykorzystanie byłoby coś jak:

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 

Oczywiście realizacja interfejs ma znacznie mniej kodu, ale to naprawdę nie ma ochoty kodu funkcjonalnej więcej. Jeśli zacznę korzystać z interfejsów w całej mojej aplikacji internetowej, kiedy będę wiedzieć, kiedy użyć funkcji wyższego rzędu? - W przeciwnym razie skończę z prostym starym rozwiązaniem OOP.

Krótko mówiąc: Kiedy należy używać interfejsów i kiedy używać funkcji wyższego rzędu? - trzeba narysować jakąś linię, albo wszystkie będą typami i interfejsami, z których zniknie piękno FP.

+1

Napisałem o tym kilka przemyśleń na moim blogu, próbując wyjaśnij związek między obiektami i funkcjami wyższego rzędu między innymi: http://bugsquash.blogspot.com/2013/08/objects-and-functional-programming.html –

Odpowiedz

10

Na interfejsach. Po pierwsze, wydaje mi się, że interfejsy można zobaczyć w postaci tylko nazwanych par funkcji. Jeśli masz:

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

to jest prawie równoważna mający parę (lub rekord) funkcji:

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

Zaletą korzystania z interfejsów jest to, że dajesz rodzaju nazwy (dostaniesz to również z nagraniami) i wyraźniej wyrażasz swoją intencję (inne komponenty mogą implementować interfejs).

Myślę, że używanie interfejsu jest dobrym pomysłem, jeśli masz więcej niż 2 funkcje, które często są przekazywane do innej funkcji - w ten sposób unikasz zbyt wielu parametrów.

Moduł lub klasa. Prawdziwą różnicą w twoim kodzie jest to, czy używać modułu z funkcjami wyższego rzędu, czy też klasy, która przyjmuje interfejs jako argument konstruktora. F # jest językiem wieloparadygmatowym, który łączy funkcjonalność i styl OO, więc myślę, że używanie klas w ten sposób jest doskonale w porządku.(Możesz nadal korzystać ze stylu funkcjonalnego podczas definiowania typów danych reprezentujących domenę itp.).

Należy pamiętać, że programowanie funkcjonalne to wszystko o składzie. To może nie być tak przydatne w tym przypadku, ale zawsze wolę pisanie kodu, który mogę skomponować, aby dodać więcej funkcji niż kodu, który wymaga ode mnie podania, gdy chcę go użyć.

Może mógłbyś napisać to tak, że Twój kod dostępu do bazy danych nie wykonuje bezpośrednio buforowanie (będzie to obejmować wszystkie zapytania do bazy danych i logiki wstępnego przetwarzania):

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

... a następnie zdefiniować typ która owija to i dodaje buforowanie (i to byłoby wtedy używane przez główny typ aplikacji internetowej - jest dość podobne do typu zdefiniowanego w twoim przykładzie, ale teraz oddzielamy dostęp do bazy danych i zapamiętywanie za pomocą dostawcy pamięci podręcznej) :

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 

Podsumowanie. Ale - myślę, że twoje podejście przy użyciu klasy, która przyjmuje ICacheProvider jest całkowicie w porządku - F # jest całkiem niezła w mieszaniu stylu funkcjonalnego i obiektowego. Zamieszczony przeze mnie przykład jest naprawdę tylko możliwym rozszerzeniem, które może być przydatne w większych projektach (jeśli chciałbyś używać aspektów funkcjonalnych i wyraźnie rozdzielać różne aspekty funkcjonalności)

+0

Również platformy testowania jednostkowego mają tendencję do gry znacznie ładniej z kpiącymi interfejsami zamiast szydzić np krotki sygnatur funkcji. –

Powiązane problemy