何时使用接口以及何时使用更高阶的函数? [英] When to use interfaces, and when to use higher order functions?

查看:117
本文介绍了何时使用接口以及何时使用更高阶的函数?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

给定一个带有以下图层的ASP.NET MVC应用程序:
$ b $ ul
  • UI(视图,CSS,Javascript等)

    li>
  • 控制器

  • 服务(包含业务逻辑和数据访问)


    没有独立数据访问层的原因是我使用SQL类型的提供程序。



    (以下代码可能不工作,因为它只是原始草稿)。
    现在想象一下名为 UserService 的服务定义为:

     模块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

    然后在我的Controllers图层中,我将有另一个名为 UserImpl 的模块,或者它也可以好的命名为 UserMemCache
    $ b

      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()

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

    使用接口,我们会有以下的实现:

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

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

    成员x.Add name =
    let result = db。添加名称
    cacheProvider.evict< @ x.GetAll()@> []
    结果

    使用情况类似于:

      type UserController(ICacheProvider cacheProvider)= 
    inherit Controller()

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

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

    很明显,接口实现的代码少得多,但它不再像真正的功能代码那样。如果我在整个网络应用程序中开始使用接口,那么我什么时候才能知道何时使用高阶函数呢? - 否则我只会得到一个简单的旧的OOP解决方案。



    所以简而言之:什么时候应该使用接口,何时使用更高阶的函数? - 必须画出一些线条,或者它们全都是类型和界面,其中FP的美丽消失。

    解决方案
  • strong> On接口。首先,我认为您可以将接口看作刚刚命名的函数对。如果你有:

      type ICacheProvider = 
    abstract Get:string - >选项<物镜>
    抽象集:string * obj - >单位

    然后这与具有一对(或一个记录)函数几乎等价: p>

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

    使用接口的好处是您可以为该类型指定一个名称(您也可以通过记录获得该名称)清楚地表达你的意图(其他组件可以实现这个接口)。



    我认为如果你有超过2个函数经常传递给一些接口其他函数一起使用 - 这样就避免了太多参数。

    模块或类。代码的真正区别在于是否使用具有高阶函数的模块或将接口作为构造函数参数的类。 F#是一种结合功能和面向对象风格的多范式语言,因此我认为以这种方式使用类是完全正确的。 (当定义数据类型来表示域等时,您仍然可以从功能风格中受益)。

    有一点需要记住的是函数式编程都是关于组合物。在这种情况下,这可能不太有用,但我总是更喜欢编写代码,以便添加更多的功能,而不是需要我在使用时提供某些内容的代码。

    也许你可以编写它,以便数据库访问代码不直接进行缓存(这将包括所有数据库查询和预处理逻辑):

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

    ...然后定义一个类型它包装了这个并添加了缓存(然后这些将被主要类型的Web应用程序使用 - 它与您在示例中定义的类型非常相似,但现在我们正在使用缓存提供程序分离数据库访问和记忆) :

      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#在混合功能和面向对象风格方面相当不错。我发布的例子实际上只是一个可能的扩展,可能在更大的项目中很有用(如果你想使用功能方面并且清楚地区分功能的不同方面)

    Given a ASP.NET MVC application with the following layers:

    • UI (Views, CSS, Javascript, etc.)
    • Controllers
    • Services (Contains business logic, and data access)

    The reason for no separate data access layer, is that I'm using SQL type provider.

    (The following code may not be working, as it's only a raw draft). Now imagine a service named UserService defined like:

    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
    

    And then in my Controllers layer, I'll have another module named UserImpl or it could just as well be named 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 
    

    The usage of this would be like:

    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
    

    Using interfaces, we would have the following implementation:

    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
    

    And the usage would be something like:

    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
    

    Obviously the interface implementation has much less code, but it doesn't really feel like functional code anymore. If I start using interfaces throughout my web app, when do I know when to use higher order functions instead? - else I'll just end up with a plain old OOP solution.

    So in short: When should interfaces be used, and when to use higher order functions? - some line has to be drawn, or it will all be types and interfaces, whereof the beauty of FP disappears.

    解决方案

    On interfaces. First of all, I think you can see interfaces as just named pairs of functions. If you have:

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

    then this is pretty much equivalent to having a pair (or a record) of functions:

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

    The benefit of using interfaces is that you give the type a name (you would get that with records too) and you are more clearly expressing your intention (other components can implement the interface).

    I think using an interface is a good idea if you have more than 2 functions that are often passed to some other function together - this way, you avoid having too many parameters.

    Module or class. The real difference in your code is whether to use module with higher-order functions or a class that takes the interface as constructor argument. F# is a multi-paradigm language that combines functional and OO style, so I think using classes in this way is perfectly fine. (You can still benefit from the functional style when defining data types to represent the domain etc.)

    One thing to keep in mind is that functional programming is all about composition. This might not be as useful in this case, but I always prefer writing code that I can compose to add more functionality rather than code that requires me to provide something when I want to use it.

    Perhaps you could write it so that your database access code does not do caching directly (this would include all the database queries and pre-processing logic):

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

    ...and then define a type that wraps this and adds caching (and this would then be used by the main type of the web application - it is quite similar to the type you defined in your example, but now we are separating the database access and memorization using a cache provider):

    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
    

    Summary. But - I think your approach using a class that takes ICacheProvider is perfectly fine - F# is pretty good in mixing functional and object oriented style. The example I posted is really just a possible extension that might be useful in bigger projects (if you wanted to use functional aspects and clearly separate different aspects of the functionality)

    这篇关于何时使用接口以及何时使用更高阶的函数?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

    查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆