web-dev-qa-db-ja.com

永続性は純粋に関数型の言語にどのように適合しますか?

コマンドハンドラーを使用して永続性を処理するパターンは、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.NewEventsIEnumerable<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などのコマンドハンドラーを呼び出す、ある種の「プレゼンテーション」コードがあります。これで、プログラムに2つのレイヤーが必要になり、 IO-コマンドハンドラとビュー-Haskellでの大きな禁止事項です。

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

もちろん、「通常の」言語では、IOはどこでも発生する可能性があり、実際に発生します。良い設計では、IOしかし、コンパイラーはそれを強制しません。

したがって、モデルとビューの分離を、モデルを永続化する必要があるときに、IOコードをプログラムのエッジにプッシュするという欲求とどのように調和させるのですか? IO分離)の2つの異なるタイプを維持しますが、それでもすべての純粋なコードから離れていますか?


更新:バウンティは24時間以内に期限切れになります。現在の回答のどちらも私の質問にまったく対応していないと思います。 @PtharienのFlameによるacid-stateに関するコメントは期待できるようですが、回答ではなく、詳細もありません。これらのポイントが無駄になるのは嫌です!

20

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

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

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

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

したがって、さまざまな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 です。
    • コントローラーがビューの状態とサブモデルの状態の両方にアクセスできることに注意してください
  • ドライバーの定義はmain :: IO ()のみです。これは、他のコンポーネントを1つのシステムに結合する簡単な作業を行います
    • ビューとサブモデルは、zoomまたは同様のコンビネータを使用して、コントローラと同じ状態タイプに持ち上げる必要があります
    • モデルは純粋なので、制限なしで使用できます
    • 結局、すべてがStateT (DataState, UIState) IO(互換性のあるタイプ)に存在し、データベースまたはストレージの実際のコンテンツを使用して実行され、IOが生成されます。
6

したがって、モデルとビューの分離を、モデルを永続化する必要がある場合に、プログラムのエッジにプッシュIOコードをプッシュするという欲求とどのように調和させるのでしょうか。

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

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

そうは言っても、状態はしばしば保存されますが、必須のプログラムデータとしてではなく、コマンドの再生を回避するためのスナップショット/キャッシュとしてのみです。

これで、プログラムに2つのレイヤーができました。IO-コマンドハンドラーとビュー-これはHaskellの大きな禁止事項です。

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

関連項目
イベントソーシング
熱心な読み取りの派生

4
FMJaguar

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

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

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

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

2
Jimmy Hoffa

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

理想的には、オブジェクト自体を拡張して状態を保存できるので、オブジェクトが「受け渡される」と、さまざまなコマンドプロセッサが何を処理しているかがわかります。

それが不可能な場合は(icky icky)、唯一の方法は、DBのような一般的なキーを使用することです。これを使用して、さまざまなコマンド間で共有できるように設定されているストアに情報を保存できます。インターフェースやコードを「開く」ことで、他のコマンド作成者もメタ情報の保存と処理にインターフェースを採用します。

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

代替ソリューション-共通のファイル(オブジェクト)を操作するさまざまなユーザーからの複数のsambaプロセスで機能するのは、ファイルシステムが拡張属性のようにファイルへのリソースの直接アタッチをサポートしていない場合、実装するモジュールを使用することですSambaプロセスの拡張属性をエミュレートする仮想ファイルシステムレイヤー。 sambaだけがそれを認識しますが、オブジェクト形式がサポートしていない場合に機能するという利点がありますが、以前の状態に基づいてファイルに対して何らかの作業を行うさまざまなsambaユーザー(コマンドプロセッサを参照)でも機能します。ファイルシステムの共通データベースにメタ情報を保存し、データベースのサイズを制御するのに役立ちます(ファイルが削除されない限り、エントリの有効期限は必要ありません)-Sambaが唯一のアクセサー(共通インターフェイス)の場合)をファイルに追加すると、ファイルシステム上のファイルに同じ機能を透過的に提供できます。それ以外の場合は、ファイルシステムの形式に固有です(オブジェクトが異なる人またはチームによって行われる場合のデータ表現の異なる形式と同様)。

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

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

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

乾杯!

1
Astara