何时使用接口,何时使用更高阶的功能?

时间:2013-08-27 06:41:05

标签: dependency-injection f# functional-programming

给定具有以下层的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

显然,接口实现的代码要少得多,但它实际上并不像功能代码那样。如果我开始在我的网络应用程序中使用界面,我何时知道何时使用更高阶的功能呢? - 否则我最终会得到一个简单的旧OOP解决方案。

简而言之:何时应该使用接口,何时使用更高阶函数? - 必须绘制一些线条,或者它们都是类型和界面,FP的美丽消失了。

1 个答案:

答案 0 :(得分:10)

在接口上。首先,我认为您可以将接口视为命名的函数对。如果你有:

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

...然后定义一个包装它的类型并添加缓存(然后这将由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#在混合功能和面向对象的风格方面相当不错。我发布的示例实际上只是一个可能的扩展,可能在更大的项目中有用(如果您想使用功能方面并明确区分功能的不同方面)