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

そして、Controllers レイヤーに、別のモジュールの名前を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)

インターフェイスを使用する利点は、型に名前を付け (レコードでも取得できます)、意図をより明確に表現できることです (他のコンポーネントがインターフェイスを実装できます)。

他の関数に一緒に渡されることが多い2つ以上の関数がある場合、インターフェースを使用することは良い考えだと思います-このようにして、パラメーターが多すぎないようにします。

モジュールまたはクラス。コードの実際の違いは、モジュールを高階関数と共に使用するか、インターフェイスをコンストラクター引数として受け取るクラスを使用するかです。F# は関数型スタイルとオブジェクト指向スタイルを組み合わせたマルチパラダイム言語なので、このようにクラスを使用してもまったく問題ないと思います。(ドメインなどを表すデータ型を定義するときは、関数型スタイルの恩恵を受けることができます。)

心に留めておくべきことの 1 つは、関数型プログラミングは合成に関するものだということです。この場合、これはあまり役に立たないかもしれませんが、使用したいときに何かを提供する必要があるコードよりも、より多くの機能を追加するために構成できるコードを書くことを常に好みます。

おそらく、データベース アクセス コードがキャッシュを直接実行しないように記述できます (これには、すべてのデータベース クエリと前処理ロジックが含まれます)。

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 に答える