12

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

显然接口实现的代码要少得多,但它不再像函数式代码了。如果我开始在整个 Web 应用程序中使用接口,我什么时候知道何时使用高阶函数?- 否则我只会得到一个普通的旧 OOP 解决方案。

简而言之:什么时候应该使用接口,什么时候使用高阶函数?- 必须画一些线,否则都是类型和接口,FP 的美感就消失了。

4

1 回答 1

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# 在混合功能和面向对象的风格方面非常好。我发布的示例实际上只是一个可能的扩展,可能在更大的项目中有用(如果您想使用功能方面并明确区分功能的不同方面)

于 2013-08-27T14:50:33.160 回答