web-dev-qa-db-ja.com

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

私は最近、C#でSOLID=をかなり極端なレベルまで実行しており、最近では基本的に、関数を作成する以外にほとんど何もしていないことに気づきました。そして、最近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コンテナーに、AsdFilteringGetStuffIGetStuffを解決し、そのインターフェースとの独自の依存関係のGeneratingGetStuffを解決するように指示します。ここで、別のフィルターが必要な場合、またはフィルターを完全に削除した場合、IGetStuffのそれぞれの実装が必要になり、IoC登録を変更するだけで済みます。インターフェースが同じである限り、何かに触れる必要はありませんwithinアプリケーション。 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つの関数は交換できなくなりました。考え直して、彼らはおそらくとにかくべきではありません。

他に何ができますか? F#プロジェクトの最後のファイルのC#SOLIDからの「コンポジションルート」の概念を模倣することができます。ほとんどのファイルは関数のコレクションにすぎないので、何らかの「レジストリ」があります。 IoCコンテナーを置き換えます。最後に、アプリケーションを実際に実行するために呼び出し、「レジストリ」の関数を使用する関数が1つあります。「レジストリ」では、タイプの関数(Guid-> Dingsリスト)が必要であることを知っています。 、これをGetDingseForIdと呼びます。これは私が呼び出すものであり、以前に定義された個々の関数ではありません。

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

let GetDingseForId id = AsdFilteredDingse GenerateDingse

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

let GetDingseForId id = GenerateDingse

これの欠点(?)は、他の関数を使用するすべての関数は高次関数でなければならないこと、そして実際の関数は、使用するall関数をマップする必要があるということです。以前に定義された関数は、後で定義された関数を呼び出すことができません。特に「レジストリ」の関数は呼び出せません。 「レジストリ」マッピングで循環依存の問題が発生する可能性もあります。

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

73
TeaDrivenDev

これは、オブジェクト指向コンストラクターの注入が機能的部分的機能アプリケーションに非常に対応していることを理解すれば簡単です。

まず、レコードタイプとしてDingsを記述します。

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

F#では、IGetStuffインターフェイスをシグネチャを持つ単一の関数に減らすことができます

Guid -> seq<Dings>

この関数を使用するclientは、それをパラメーターとして受け取ります。

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>よりもmoreジェネリックですが、それは問題ではありません。 ClientGenerateDingseで構成するだけの場合は、次のように使用できます。

let result = Client GenerateDingse

これは、Dingから3つのGenerateDingse値をすべて返します。

デコレーター

オリジナルのデコレーターは少し難しいですが、それほど難しいものではありません。一般的に、Decorated(内部)型をコンストラクター引数として追加する代わりに、パラメーター値として関数に追加するだけです。

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>

私たちが探しているものだけです!

次のようにClientcomposedとともに使用できるようになりました。

let result = Client composed

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

最初にcomposed関数を定義する必要はありませんhave。その場で作成することもできます:

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

これは[{Lol = "asd"; Rofl = "ASD";}]も返します。

代替デコレータ

前の例はうまく機能しますが、really同様に機能しません。ここに代替があります:

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

繰り返しますが、Clientcomposedとともに次のように使用できます。

let result = Client composed

またはこのようなインライン:

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

F#を使用してアプリケーション全体を構成するためのその他の例と原則については、 F#を使用した機能アーキテクチャに関するオンラインコース を参照してください。

オブジェクト指向の原理とそれらが関数型プログラミングにどのようにマッピングされるかについての詳細は、 (SOLIDの原理とそれらがFPにどのように適用されるか)に関する私のブログ投稿 を参照してください。

58
Mark Seemann