Когда использовать интерфейсы и когда использовать функции более высокого порядка?

Учитывая приложение ASP.NET MVC со следующими слоями:

  • UI (Представления, CSS, Javascript и т.д.).
  • Контроллеры
  • Услуги (Содержит бизнес-логику и доступ к данным)

Причина отсутствия отдельного уровня доступа к данным заключается в том, что я использую поставщик типа SQL.

(Следующий код может не работать, поскольку это только черновик). Теперь представьте себе службу с именем UserService, определенную как:

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

И затем на моем уровне контроллеров у меня будет еще один модуль с именем UserImpl или его можно просто назвать 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 

Использование этого будет выглядеть так:

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

Используя интерфейсы, мы будем иметь следующую реализацию:

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

И использование будет выглядеть примерно так:

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

Очевидно, что в реализации интерфейса гораздо меньше кода, но он больше не похож на функциональный код. Если я начну использовать интерфейсы в своем веб-приложении, когда я узнаю, когда использовать функции более высокого порядка? - иначе я просто окажусь простым старым решением ООП.

Итак, вкратце: когда следует использовать интерфейсы и когда использовать функции более высокого порядка? - какая-то линия должна быть нарисована, или все это будут типы и интерфейсы, из которых исчезает красота FP.

Ответ 1

На интерфейсах. Прежде всего, я думаю, вы можете видеть интерфейсы как только названные пары функций. Если у вас есть:

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

то это в значительной степени эквивалентно наличию пары (или записи) функций:

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

Преимущество использования интерфейсов заключается в том, что вы указываете тип имени (вы также получите это с помощью записей), и вы более четко выражаете свое намерение (другие компоненты могут реализовать интерфейс).

Я думаю, что использование интерфейса - хорошая идея, если у вас есть более двух функций, которые часто передаются вместе с другой функцией. Таким образом, вы избегаете слишком большого количества параметров.

Модуль или класс.. Реальная разница в вашем коде заключается в том, следует ли использовать модуль с функциями более высокого порядка или класс, который принимает интерфейс как аргумент конструктора. F # - это язык с несколькими парадигмами, который сочетает в себе функциональный стиль и стиль OO, поэтому я считаю, что использование классов таким образом прекрасно. (Вы можете использовать функциональный стиль при определении типов данных для представления домена и т.д.)

Следует иметь в виду, что функциональное программирование касается композиции. Это может быть не так полезно в этом случае, но я всегда предпочитаю писать код, который я могу создать, чтобы добавить больше функциональности, а не код, который требует от меня чего-то, когда я хочу его использовать.

Возможно, вы могли бы написать его так, чтобы ваш код доступа к базе данных не выполнял кеширование напрямую (это включало бы все запросы к базе данных и логику предварительной обработки):

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

... и затем определите тип, который обертывает это и добавляет кеширование (и тогда это будет использоваться основным типом веб-приложения - оно очень похоже на тип, определенный вами в вашем примере, но теперь мы разделяя доступ к базе данных и запоминание с помощью поставщика кеша):

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

Резюме. Но - я думаю, что ваш подход с использованием класса, который принимает ICacheProvider, отлично - F # довольно хорош в смешении функционального и объектно-ориентированного стиля. Пример, который я опубликовал, на самом деле является просто возможным расширением, которое может быть полезно в больших проектах (если вы хотите использовать функциональные аспекты и четко разделять различные аспекты функциональности)