永続性は純粋に機能的な言語にどのように適合しますか?


18

コマンドハンドラーを使用して永続性を処理するパターンは、IO関連のコードをできるだけ薄くしたい純粋に機能的な言語にどのように適合しますか?


オブジェクト指向言語でドメイン駆動設計を実装する場合、コマンド/ハンドラーパターンを使用して状態の変更を実行するのが一般的です。この設計では、コマンドハンドラーはドメインオブジェクトの上に配置され、リポジトリの使用やドメインイベントの発行など、永続性に関連する退屈なロジックを担当します。ハンドラーは、ドメインモデルのパブリックフェイスです。UIなどのアプリケーションコードは、ドメインオブジェクトの状態を変更する必要があるときにハンドラーを呼び出します。

C#のスケッチ:

public class DiscardDraftDocumentCommandHandler : CommandHandler<DiscardDraftDocument>
{
    IDraftDocumentRepository _repo;
    IEventPublisher _publisher;

    public DiscardDraftCommandHandler(IDraftDocumentRepository repo, IEventPublisher publisher)
    {
        _repo = repo;
        _publisher = publisher;
    }

    public override void Handle(DiscardDraftDocument command)
    {
        var document = _repo.Get(command.DocumentId);
        document.Discard(command.UserId);
        _publisher.Publish(document.NewEvents);
    }
}

documentドメインオブジェクトは、((「あなたは既に破棄されていた文書を破棄することはできません」または「ユーザーが文書を破棄する権限を持つべきである」のような)ビジネスルールを実装するため、我々は公開する必要があるドメインイベントを発生させるための責任があるdocument.NewEventsだろうIEnumerable<Event>おそらく含まれていますDocumentDiscarded)イベントを。

これは素晴らしいデザインです-拡張が簡単で(ドメインモデルを変更せずに新しいコマンドハンドラーを追加することで新しいユースケースを追加できます)、オブジェクトの永続化方法に依存しません(MongoのNHibernateリポジトリを簡単に交換できます)リポジトリ、またはRabbitMQパブリッシャーをEventStoreパブリッシャーに交換します)これにより、偽物やモックを使用して簡単にテストできます。また、モデル/ビューの分離に従います-コマンドハンドラーは、バッチジョブ、GUI、またはREST APIのいずれで使用されているかわかりません。


Haskellのような純粋に機能的な言語では、おおよそ次のようにコマンドハンドラーをモデル化できます。

newtype CommandHandler = CommandHandler {handleCommand :: Command -> IO Result)
data Result a = Success a | Failure Reason
type Reason = String

discardDraftDocumentCommandHandler = CommandHandler handle
    where handle (DiscardDraftDocument documentID userID) = do
              document <- loadDocument documentID
              let result = discard document userID :: Result [Event]
              case result of
                   Success events -> publishEvents events >> return result
                   -- in an event-sourced model, there's no extra step to save the document
                   Failure _ -> return result
          handle _ = return $ Failure "I expected a DiscardDraftDocument command"

ここに私が理解するのに苦労している部分があります。通常、GUIやREST APIなど、コマンドハンドラーを呼び出す何らかの「プレゼンテーション」コードがあります。そのため、プログラムにIOを実行する必要がある2つのレイヤーがあります。コマンドハンドラーとビューです。これはHaskellの大きな問題です。

私の知る限り、ここには2つの対立する力があります。1つはモデル/ビューの分離であり、もう1つはモデルを維持する必要があることです。モデルをどこかに永続化するにはIOコードが必要ですが、モデル/ビューの分離では、他のすべてのIOコードとともにプレゼンテーション層に配置することはできません。

もちろん、「通常の」言語では、IOはどこでも発生する可能性があります(実際に発生します)。優れた設計では、さまざまなタイプのIOを分離しておく必要がありますが、コンパイラはそれを強制しません。

それでは、モデルを永続化する必要があるときに、IOコードをプログラムの端にプッシュしたいという欲求と、モデル/ビューの分離をどのように調和させるのでしょうか?2つの異なるタイプのIOを別々保ちながら、すべての純粋なコードからどのように離すのですか


更新:賞金は24時間以内に期限切れになります。現在の回答のいずれかが私の質問に対処しているとはまったく感じていません。@ Ptharien's Flameのコメントacid-stateは有望に思えますが、それは答えではなく、詳細に欠けています。これらのポイントが無駄になるのは嫌だ!


1
おそらく、Haskellのさまざまな永続化ライブラリの設計を調べると役立つでしょう。特に、acid-stateあなたが記述しているものに近いようです
プサリアンの炎14年

1
acid-stateそのリンクに感謝します。API設計の観点からは、まだ拘束されているようIOです。私の質問は、永続フレームワークがより大きなアーキテクチャにどのように適合するかについてです。acid-stateプレゼンテーション層と一緒に使用するオープンソースアプリケーションを知っていますか?
ベンジャミンホジソン14年

QueryそしてUpdateモナドはかなり遠くから削除されIO、実際に、。私は答えの中で簡単な例を挙げようとします。
プサリアンの炎14

このようにコマンド/ハンドラーパターンを使用している読者には、トピックから外れてしまう危険性があるため、Akka.NETをチェックアウトすることをお勧めします。俳優モデルはここにぴったりのように感じます。Pluralsightには素晴らしいコースがあります。(私は単なるボットであり、プロモーションボットではないことを誓います。)
RJB

回答:


6

Haskellでコンポーネントを分離する一般的な方法は、モナド変換スタックを使用することです。これについては、以下で詳しく説明します。

いくつかの大規模なコンポーネントを持つシステムを構築していると想像してください:

  • ディスクまたはデータベースと通信するコンポーネント(サブモデル)
  • ドメイン(モデル)で変換を行うコンポーネント
  • ユーザーと対話するコンポーネント(ビュー)
  • ビュー、モデル、およびサブモデル(コントローラー)間の接続を記述するコンポーネント
  • システム全体(ドライバー)をキックスタートするコンポーネント

優れたコードスタイルを維持するために、これらのコンポーネントを疎結合に保つ必要があると判断しました。

そのため、さまざまなMTLクラスを使用して、各コンポーネントを多態的にコーディングします。

  • サブモデルのすべての関数は型です MonadState DataState m => Foo -> Bar -> ... -> m Baz
    • DataState データベースまたはストレージの状態のスナップショットの純粋な表現です
  • モデル内のすべての関数は純粋です
  • ビュー内のすべての関数はタイプです MonadState UIState m => Foo -> Bar -> ... -> m Baz
    • UIState ユーザーインターフェイスの状態のスナップショットの純粋な表現です
  • コントローラーのすべての機能はタイプです MonadState (DataState, UIState) m => Foo -> Bar -> ... -> m Baz
    • コントローラーがビューの状態とサブモデルの状態の両方にアクセスできることに注意してください。
  • ドライバーには1つの定義しかmain :: IO ()ありません。これは、他のコンポーネントを1つのシステムに結合するという、ごく簡単な作業を行います
    • ビューとサブモデルは、使用しているコントローラーzoomまたは同様のコンビネーターと同じ状態タイプに持ち上げる必要があります
    • モデルは純粋なので、制限なしで使用できます
    • 最終的には、すべてが(互換性のある型)StateT (DataState, UIState) IOに存在し、データベースまたはストレージの実際のコンテンツで実行され、生成されIOます。

1
これは素晴らしいアドバイスであり、まさに私が探していたものです。ありがとう!
ベンジャミンホジソン14年

2
私はこの答えを消化しています。このアーキテクチャでの「サブモデル」の役割を明確にしてください。IOを実行せずに「ディスクまたはデータベースと通信」する方法 「DataStateデータベースまたはストレージの状態のスナップショットの純粋な表現である」という意味について特に混乱しています。おそらく、データベース全体をメモリにロードするつもりはありません!
ベンジャミンホジソン14年

1
このロジックのC#実装に関するあなたの考えをぜひ見たいです。私はあなたに賛成票を贈ることができると思いませんか?;-)
RJB

1
@RJB残念ながら、C#開発チームに賄briを払わなければ、言語のより高い種類を許可できません。
プサリアンの炎

4

それでは、モデルを永続化する必要があるときに、IOコードをプログラムの端にプッシュしたいという欲求と、モデルとビューの分離をどのように調和させるのでしょうか?

モデルを永続化する必要がありますか?多くのプログラムでは、状態が予測不能であるためモデルを保存する必要があり、どの操作でもモデルを変化させる可能性があるため、モデルの状態を知る唯一の方法は直接アクセスすることです。

シナリオで、一連のイベント(検証および受け入れられたコマンド)が常に状態を生成できる場合、それは必ずしも状態ではなく、永続化する必要があるイベントです。状態は、イベントを再生することで常に生成できます。

そうは言っても、多くの場合、状態は保存されますが、本質的なプログラムデータとしてではなく、コマンドのリプレイを避けるためのスナップショット/キャッシュとして保存されます。

そのため、プログラムにIOを実行する必要がある2つのレイヤーがあります。コマンドハンドラーとビューです。これはHaskellの大きな問題です。

コマンドが受け入れられると、イベントはプログラムの同じレイヤーにある2つの宛先(イベントストレージとレポートシステム)に通信されます。

関連
トピックイベントソーシング
Eager Read Derivation


2
私はイベントソーシングに精通しています(上記の例で使用しています!)。髪の毛が割れないようにするために、イベントソーシングは永続性の問題へのアプローチであると言います。いずれにせよ、イベントソーシングは、コマンドハンドラーでドメインオブジェクトをロードする必要をなくしません。コマンドハンドラーは、オブジェクトがイベントストリーム、ORM、またはストアドプロシージャのどちらから来たかを知りません。リポジトリから取得するだけです。
ベンジャミンホジソン14年

1
あなたの理解は、複数のIOを作成するためにビューとコマンドハンドラーを結合しているようです。私の理解では、ハンドラーはイベントを生成し、それ以上の関心はありません。このインスタンスのビューは(技術的には同じアプリケーション内であっても)別のモジュールとして機能し、コマンドハンドラーに結合されていません。
FMJaguar

1
私たちは交差目的で話しているのではないかと思います。「ビュー」と言うときは、プレゼンテーション層全体について話します。これは、REST APIまたはモデルビューコントローラーシステムの場合があります。(ビューをMVCパターンのモデルから分離することに同意します。)基本的には、「コマンドハンドラーを呼び出すもの」を意味します。
ベンジャミンホジソン14年

2

すべての非IOアクティビティのために、IO集約型アプリケーションにスペースを入れようとしています。残念ながら、あなたのような典型的なCRUDアプリはIO以外のことはほとんどしません。

関連する分離については十分理解できていると思いますが、永続性IOコードをプレゼンテーションコードからいくつかのレイヤーに配置しようとする場合、問題の一般的な事実は、コントローラーのどこかで呼び出す必要がありますプレゼンテーション層に近すぎると感じるかもしれない永続化レイヤー-しかし、それはそのタイプのアプリが他にほとんど持っていないことの偶然です。

プレゼンテーションと永続性は、基本的に、ここで説明しているタイプのアプリ全体を構成しています。

多くの複雑なビジネスロジックとデータ処理を備えた同様のアプリケーションについて頭の中で考えると、プレゼンテーションIOや永続性IOからどのようにうまく分離されているか想像できると思います。どちらについても何も知らない必要があります。あなたが現在抱えている問題は、そもそもその問題を持たないタイプのアプリケーションの問題の解決策を見ようとすることによって引き起こされる知覚的な問題です。


1
CRUDシステムで永続性とプレゼンテーションを組み合わせるのは問題ないと言っています。これは私には理にかなっているようです。ただし、CRUDについては言及しませんでした。具体的には、DDDについてお聞きします。DDDでは、複雑な相互作用、永続層(コマンドハンドラー)、およびその上のプレゼンテーション層を備えたビジネスオブジェクトがあります。薄い IOラッパーを維持しながら、2つのIOレイヤーをどのように分離しますか?
ベンジャミンホジソン14年

1
NB、質問で説明したドメインは非常に複雑になる可能性があります。おそらくドラフト文書を破棄することは、いくつかの関連する権限チェックの対象となるか、同じドラフトの複数のバージョンを処理する必要があるか、通知を送信する必要があるか、アクションが別のユーザーによる承認を必要とするか、ドラフトが多数のファイナライズまでのライフサイクルステージ...
ベンジャミンホジソン14年

2
@BenjaminHodgson頭の中でこのような状況にDDDやその他のOO設計手法を混ぜることは強くお勧めしますが、混乱するだけです。はい、純粋なFPでビットやボブルのようなオブジェクトを作成できますが、それらに基づいた設計アプローチは必ずしも最初の到達範囲ではありません。上記のシナリオでは、2つのIOと純粋なコードの間で通信するコントローラーを想定しています:プレゼンテーションIOはコントローラーに入り、コントローラーから要求され、コントローラーは物事を純粋なセクションと永続化セクションに渡します。
ジミーホファ14年

1
@BenjaminHodgsonでは、純粋なコードがすべて存在するバブルを想像できます。どのようなデザインでも、必要なレイヤーや空想があります。このバブルのエントリポイントは、プレゼンテーション、永続性、および純粋なピースの間の通信を行う「コントローラー」(おそらく誤って)と呼ぶ小さなピースになります。このようにして、あなたの永続性はプレゼンテーションのことも純粋なことも知りませんし、その逆も同様です-そしてこれはあなたの純粋なシステムのバブルの上にあるこの薄い層にIOのものを保持します。
ジミーホファ14年

2
@BenjaminHodgsonこの「スマートオブジェクト」アプローチは、FPにとって本質的に悪いアプローチです。FPのスマートオブジェクトの問題は、結合が多すぎて一般化が少なすぎることです。最終的には、データとそれに関連付けられた機能になります。FPでは、一般化する関数を実装できるようにデータが機能と疎結合し、複数のタイプのデータ間で機能するようになっています。ここに私の答えの読み取りを持っている:programmers.stackexchange.com/questions/203077/203082#203082
ジミー・ホッファ

1

あなたの質問を理解できる限り(それは分からないかもしれませんが、私は2セントを投じると思います)、あなたは必ずしもオブジェクト自体にアクセスできるわけではないので、あなた自身のオブジェクトデータベースが必要です。時間が経過すると有効期限が切れます)。

理想的には、オブジェクト自体を強化してその状態を保存できるため、「渡される」ときに、さまざまなコマンドプロセッサがそれらがどのように処理されているかを知ることができます。

それが不可能な場合(icky icky)、唯一の方法は、DBのような一般的なキーを使用することです。これを使用して、異なるコマンド間で共有できるようにセットアップされたストアに情報を保存できます。インターフェイスやコードを「オープン」して、他のコマンドライターがメタ情報の保存と処理にインターフェイスを採用するようにします。

ファイルサーバーの分野では、sambaには、ホストOSが提供するものに応じて、アクセスリストや代替データストリームなどを保存するさまざまな方法があります。理想的には、sambaはファイルシステムでホストされ、ファイルの拡張属性を提供します。「Linux」での「xfs」の例-拡張属性をファイルとともにコピーするコマンドが増えています(デフォルトでは、Linuxのほとんどのユーティリティは拡張属性なしで「成長」します)。

代替ソリューション-共通ファイル(オブジェクト)を操作するさまざまなユーザーの複数のsambaプロセスで機能します。ファイルシステムが拡張属性のようにリソースをファイルに直接接続することをサポートしていない場合は、実装するモジュールを使用しますsambaプロセスの拡張属性をエミュレートする仮想ファイルシステムレイヤー。sambaのみがそれを知っていますが、オブジェクト形式がサポートしていない場合でも動作するという利点がありますが、以前の状態に基づいてファイルを操作するさまざまなsambaユーザー(コマンドプロセッサを参照)でも動作します。ファイルシステムの共通データベースにメタ情報を保存します。これは、データベースのサイズの制御に役立ちます(そして、

使用している実装に固有の詳細な情報が必要な場合には役に立たないかもしれませんが、概念的には、両方の問題セットに同じ理論を適用できます。あなたが望むことをするためのアルゴリズムと方法を探していたなら、それは助けになるかもしれません。特定のフレームワークでより具体的な知識が必要な場合は、あまり役に立たないかもしれません... ;-)

ところで、私が「自己期限切れ」と言う理由は、どのオブジェクトが存在し、どのくらいの期間持続するかを知っている場合、明確ではないからです。オブジェクトがいつ削除されるかを直接知る方法がない場合は、独自のmetaDBをトリミングして、ユーザーが長い間オブジェクトを削除してから古いまたは古いメタ情報でいっぱいにならないようにする必要があります。

オブジェクトの有効期限/削除のタイミングがわかっている場合、ゲームの先を行っており、metaDBから同時に期限切れにすることができますが、そのオプションがあるかどうかは明確ではありませんでした。

乾杯!


1
私には、これはまったく異なる質問に対する答えのように思えます。ドメイン駆動設計のコンテキストで、純粋に機能的なプログラミングのアーキテクチャに関するアドバイスを探していました。あなたのポイントを明確にしてください。
ベンジャミンホジソン14年

純粋に機能的なプログラミングパラダイムでのデータの永続性について尋ねています。ウィキペディアの引用:「純粋に機能的」とは、プログラムの実行環境にあるエンティティの破壊的な変更(更新)を除外するアルゴリズム、データ構造、またはプログラミング言語を記述するために使用されるコンピューティングの用語です。====定義により、データの永続性は無関係であり、データを変更しないものには使用できません。厳密に言えば、あなたの質問に対する答えはありません。私はあなたが書いたもののより緩やかな解釈を試みていました。
アスタラ14年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.