1. ホーム
  2. f#

F#によるアプリケーションアーキテクチャ/構成

2023-10-05 03:20:18

質問

私は最近、C#でSOLIDをかなり極端なレベルまでやっていて、ある時点で、最近は本質的に関数を構成すること以外はあまりやっていないことに気づきました。そして、最近F#を再び見始めた後、おそらく私が今やっていることの多くにとって、F#がより適切な言語選択であると考えました。そこで、概念実証として、現実世界のC#プロジェクトをF#に移植してみたいと思っています。実際のコードは (非常に無作法な方法で) 実行できると思いますが、C# と同様に柔軟な方法で作業できるようなアーキテクチャがどのようなものなのか、想像できません。

私が言いたいのは、私は IoC コンテナを使用して構成するたくさんの小さなクラスとインターフェースを持っていて、Decorator や Composite のようなパターンもよく使用するということです。この結果、(私の考えでは)非常に柔軟で進化可能な全体的なアーキテクチャとなり、アプリケーションのどの時点でも簡単に機能を置き換えたり拡張したりすることができます。必要な変更の大きさによっては、インターフェイスの新しい実装を書き、IoC登録でそれを置き換えるだけで終わるかもしれません。たとえ変更がより大きいとしても、アプリケーションの残りの部分が単に以前のままである間に、私はオブジェクトグラフの一部を置き換えることができます。

F#では、クラスやインターフェースがなく(できることは知っていますが、実際の関数型プログラミングをしたいときには、それは重要ではないと思います)、コンストラクタ注入もなく、IoCコンテナもないのです。高次の関数を使用した Decorator パターンのようなものができることは知っていますが、それはコンストラクタ注入を持つクラスと同じ種類の柔軟性と保守性を与えてくれないようです。

これらの C# 型を考えてみましょう。

public class Dings
{
    public string Lol { get; set; }

    public string Rofl { get; set; }
}

public interface IGetStuff
{
    IEnumerable<Dings> For(Guid id);
}

public class AsdFilteringGetStuff : IGetStuff
{
    private readonly IGetStuff _innerGetStuff;

    public AsdFilteringGetStuff(IGetStuff innerGetStuff)
    {
        this._innerGetStuff = innerGetStuff;
    }

    public IEnumerable<Dings> For(Guid id)
    {
        return this._innerGetStuff.For(id).Where(d => d.Lol == "asd");
    }
}

public class GeneratingGetStuff : IGetStuff
{
    public IEnumerable<Dings> For(Guid id)
    {
        IEnumerable<Dings> dingse;

        // somehow knows how to create correct dingse for the ID

        return dingse;
    }
}

IoCコンテナに解決するために AsdFilteringGetStuff に対して IGetStuffGeneratingGetStuff を、そのインターフェイスを持つ独自の依存関係のために使用します。さて、もし別のフィルタが必要になったり、フィルタを完全に削除したりする場合には、それぞれの実装の IGetStuff を実装し、IoCの登録を変更するだけです。インターフェイスが同じである限り、私は何も触る必要はありません。 内の を触る必要はありません。OCP と LSP、DIP によって有効化されます。

さて、F#ではどうすればいいのでしょうか?

type Dings (lol, rofl) =
    member x.Lol = lol
    member x.Rofl = rofl

let GenerateDingse id =
    // create list

let AsdFilteredDingse id =
    GenerateDingse id |> List.filter (fun x -> x.Lol = "asd")

このようにコードが少なくなるのはいいのですが、柔軟性が失われます。そう、私は AsdFilteredDingse または GenerateDingse を同じ場所で呼び出すことができます。しかし、呼び出し先でハードコーディングすることなく、どちらを呼び出すかをどのように決定すればよいのでしょうか?また、これら2つの関数は交換可能ですが、現在、私は、以下の内部でジェネレーター関数を置き換えることができません。 AsdFilteredDingse の中のジェネレータ関数を置き換えることができません。これはあまり良いことではありません。

次の試みです。

let GenerateDingse id =
    // create list

let AsdFilteredDingse (generator : System.Guid -> Dings list) id =
    generator id |> List.filter (fun x -> x.Lol = "asd")

AsdFilteredDingseを高次関数にすることで合成可能な状態にしましたが、この2つの関数はもう互換性がありません。しかし、この2つの関数はもう互換性がありません。よく考えてみると、いずれにしても互換性はないはずです。

他に何ができるでしょうか。F# プロジェクトの最後のファイルで、C# SOLID の "composition root" の概念を模倣することができました。ほとんどのファイルは単なる関数のコレクションで、次に IoC コンテナの代わりになるある種の "registry" があり、最後に実際にアプリケーションを実行するために呼び出す関数が 1 つあり、それは "registry" からの関数を使用しています。レジストリでは、(Guid -> Dings list) 型の関数が必要なことがわかっています。 GetDingseForId . これは、私が呼び出すもので、先に定義した個々の関数ではありません。

デコレータの場合、定義は次のようになります。

let GetDingseForId id = AsdFilteredDingse GenerateDingse

フィルタを解除するには、次のように変更します。

let GetDingseForId id = GenerateDingse

この欠点(?)は、他の関数を使用するすべての関数が高次関数でなければならないことで、私の "registry" は次のようにマッピングしなければならないでしょう。 すべて なぜなら、先に定義された実際の関数は、後に定義された関数、特に "registry"からの関数を呼び出すことができないからです。また、"registry" のマッピングで循環依存の問題にぶつかるかもしれません。

この中に意味があるのでしょうか?保守性と進化性(テスト可能であることは言うまでもない)のあるF#アプリケーションを実際に構築するにはどうしたらよいのでしょうか?

どのように解決するのか?

オブジェクト指向のコンストラクタ・インジェクションが機能的なコンストラクタ・インジェクションに非常によく対応していることを理解すれば、これは簡単です。 部分的な関数の適用 .

まず最初に書くのは Dings をレコード型として記述します。

type Dings = { Lol : string; Rofl : string }

F#では IGetStuff というシグネチャを持つ1つの関数に還元できます。

Guid -> seq<Dings>

A クライアント がこの関数を使うと、パラメータとして受け取ることになります。

let Client getStuff =
    getStuff(Guid("055E7FF1-2919-4246-876E-1DA71980BE9C")) |> Seq.toList

の署名は Client 関数は

(Guid -> #seq<'b>) -> 'b list

見ての通り、対象のシグネチャの関数を入力として受け取り、リストを返します。

ジェネレータ

ジェネレータ機能は簡単に書くことができます。

let GenerateDingse id =
    seq {
        yield { Lol = "Ha!"; Rofl = "Ha ha ha!" }
        yield { Lol = "Ho!"; Rofl = "Ho ho ho!" }
        yield { Lol = "asd"; Rofl = "ASD" } }

GenerateDingse 関数はこのようなシグネチャを持っています。

'a -> seq<Dings>

これは実際には より よりも一般的です。 Guid -> seq<Dings> よりも一般的ですが、それは問題ではありません。を合成したいだけなら ClientGenerateDingse というように、単純に使うことができます。

let result = Client GenerateDingse

この場合、3つの Ding の値から GenerateDingse .

デコレーター

オリジナルのDecoratorは少し難しいですが、それほどでもありません。一般的には、Decorated (inner) 型をコンストラクタの引数として追加するのではなく、関数のパラメータ値として追加すればよいのです。

let AdsFilteredDingse id s = s |> Seq.filter (fun d -> d.Lol = "asd")

この関数はこのようなシグネチャを持っています。

'a -> seq<Dings> -> seq<Dings>

これは私たちが望むものとは少し違いますが、これを合成するのは簡単です。 GenerateDingse :

let composed id = GenerateDingse id |> AdsFilteredDingse id

composed 関数は、署名

'a -> seq<Dings>

ちょうど私たちが探しているものです

これで Client と共に composed のようにします。

let result = Client composed

のみを返します。 [{Lol = "asd"; Rofl = "ASD";}] .

あなたは を持つ を定義する必要があります。 composed 関数を最初に定義する必要があります。また、その場で合成することもできます。

let result = Client (fun id -> GenerateDingse id |> AdsFilteredDingse id)

これはまた [{Lol = "asd"; Rofl = "ASD";}] .

代替デコレータ

前の例はうまく動作しますが 本当に 同様の機能を装飾する。以下はその代替案です。

let AdsFilteredDingse id f = f id |> Seq.filter (fun d -> d.Lol = "asd")

この関数は、シグネチャを持ちます。

'a -> ('a -> #seq<Dings>) -> seq<Dings>

ご覧のように f の引数は同じシグネチャを持つ別の関数なので、よりDecoratorのパターンに似ています。このように構成することができます。

let composed id = GenerateDingse |> AdsFilteredDingse id

ここでも Client と共に composed のようにします。

let result = Client composed

またはこのようにインラインで記述します。

let result = Client (fun id -> GenerateDingse |> AdsFilteredDingse id)

F#でアプリケーション全体を構成するための例と原則は、以下を参照してください。 オンラインコース「F#による機能的アーキテクチャ」をご覧ください。 .

オブジェクト指向の原則と、それがどのように関数型プログラミングに対応するかについては SOLIDの原則とそれがどのようにFPに適用されるかについての私のブログ記事 .