F#によるアプリケーションアーキテクチャ/構成
質問
私は最近、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
に対して
IGetStuff
と
GeneratingGetStuff
を、そのインターフェイスを持つ独自の依存関係のために使用します。さて、もし別のフィルタが必要になったり、フィルタを完全に削除したりする場合には、それぞれの実装の
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>
よりも一般的ですが、それは問題ではありません。を合成したいだけなら
Client
と
GenerateDingse
というように、単純に使うことができます。
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に適用されるかについての私のブログ記事 .
関連
-
[解決済み] Inversion of ControlとDependency Injectionの比較
-
[解決済み】PythonでIoC / DIが一般的でないのはなぜ?
-
[解決済み】継承と合成の違いについて
-
[解決済み] foldとreduceの違い?
-
[解決済み] F#からOCamlへの変更 [終了しました]。
-
[解決済み] OCaml/F#の関数はなぜデフォルトで再帰的でないのですか?
-
[解決済み] F#の名前空間とモジュールの違いは何ですか?
-
[解決済み] F#: mutableとrefの比較
-
[解決済み] F#は同じプロジェクト内の別のファイルで型/モジュールを定義/使用する
-
オプションタイプのリストを、noneでない要素だけに凝縮する最良の方法とは?
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン