LoginSignup
20
14

More than 3 years have passed since last update.

ADRの親が贈るAction Domain Responder(ADR)を実装する際の注意点の和訳

Posted at

はじめに

Paul M. JonesさんはADRの親です

Action-domain-responder(ADR)は、Webアプリケーションにより適したModel-view-controller(MVC)の改良版としてPaul M. Jonesによって提案されたソフトウェアアーキテクチャパターンです.

ソース: https://en.wikipedia.org/wiki/Action%E2%80%93domain%E2%80%93responder

この記事はPaul M. Jonesさんが掲載しているADRを実装する際の注意書きDeepLをベースに和訳したものです. (意訳と補足を含みます

実装上の注意点

Action Domain Responder は、ユーザーインターフェースのパターンです。それ自体がアプリケーション・アーキテクチャ全体ではありません。このセクションでは、アプリケーション・アーキテクチャの中でADRと一緒に使われる可能性のある他のコンポーネント、コラボレーション、パターンについて説明し、ADRの実装者からの注意事項や提案を示します。

Action

Action の役割は意図的に非常に限定されています。Actionは、HTTP リクエストからの入力を収集し、その入力をドメインに渡した後、コントロールをResponderに渡すだけです。ビジネスロジックはDomainでのみ扱い、プレゼンテーションロジックはResponderでのみ扱う必要があります。

Action にif/thenブロック、try/catchブロック、ループなどが含まれている場合は、Actionの処理が多すぎると言えます。唯一の例外は、HTTPリクエストにデフォルト値がない場合に、アクションがユーザーの入力に対してデフォルト値を提供することです。これはif/thenブロックではなく、三項演算子で簡単に処理できます。

補足

if/thenブロックではなく、三項演算子で簡単に処理できます。

三項演算子はあくまでも手段の1つです。
例えばjsだと三項演算子以外だと、a || ba && b論理和代入等他にもこの目的を達成する為の沢山の手段があります.

ActionはどのようにしてHTTPリクエストを受け取るべきか?

HTTP リクエストは、Actionのコンストラクタに注入することもできますし、Actionロジックを呼び出すメソッドの引数として渡すこともできます。それぞれの方法で HTTP リクエストを渡すことができますが、それぞれにトレードオフがあります。

Actionが入力を検証すべきか?

ユーザー・インターフェースコンポーネントは、ドメイン・ロジックの入力検証を行うべきではありません。

ユーザーの入力を Domain に渡し、ドメインロジックで検証を行うようにするのがよいでしょう。Domainは、入力が無効な場合、ドメイン固有のメッセージを表示して報告します。

Action がDomainの入力値であるDTOを構築するのは可能ですか?

はい. ただし、Data Transfer Objectの構築は、条件なしで完了しなければならないことに注意してください。DTOの構築で例外やエラーが発生する可能性がある場合は、必要な入力をDomainに渡し、DomainにDTOを構築させ、そのDTOを必要なドメイン・ロジックに渡す方が良いでしょう。

ActionはCommand Busを使えますか?

Command Busは、ユーザーインターフェースのパターンではなく、ドメインロジックのパターンであり、Actionではなく、Domainで使用する必要があります。この点についての詳しい説明は、Command Bus and Action-Domain-Responderを参照してください。

ドメインの例外を Action で処理するべきか?

ユーザーインターフェース要素は、ドメインロジックの例外処理を担当すべきではありません。Domainは独自の例外処理を行い、その処理結果を Action に報告します。

ActionResponderを起動する前に操作できるか?

レスポンダの実装によっては、Actionセットのデータ要素を個別に入力する必要があったり、呼び出しの前に他のメソッドの呼び出しを必要とする場合があります。

たとえば、これの代わりに:

return $this->responder->createResponse($request, $payload);

次のようにするのも合理的です。

$this->responder->setRequest($request);
$this->responder->setPayload($payload);
return $this->responder->createResponse();

ただし、条件付きのロジックが必要ないように注意しましょう。 すべてのプレゼンテーションロジックは Responder に置くべきで、 Action は値を渡してそれを呼び出すだけにしましょう。

Actionは、レスポンダを呼び出す代わりに、Responderを返すことができますか?

このパターンの初期のドラフトでは、"ActionResponderを返すことがあり、Responderはレスポンスを返すために呼び出され、レスポンスは自分自身を送信するために呼び出されます。 "と記載されていました。

しかし、よく考えてみると、Actionを呼び出すコードは、なぜレスポンス以外のものを返す必要があるのでしょうか。もし、ResponderがHTTP Responseを構築する方法を変更するロジックがあるとすれば、それはResponderの中に組み込まれるか、構成されるのがベストでしょう。

このように、レスポンスを直接返すのではなく、他の何かから呼び出されてレスポンスを作成するようにResponderを返すことは、懸念事項を適切に分離することにはなりますが、このパターンの劣った形と考えるべきです。

インターフェイス全体で1つのActionだけを使う事は可能か?

Actionの責任範囲は意図的に非常に限定されているため、すべてのユーザー・インターフェース・インタラクションを処理する単一のジェネリックなActionクラスを作成することは可能です。しかし、そうすると、依存性を注入するシステムが使えなくなる可能性があります。また、実装者は Action に必要な DomainResponder のロジックへのアクセスを与える方法や、さまざまなケースでの入力の収集方法を考えなければならないでしょう。

そのような実装の一つがArbiterです。
要するに、ルーターや他のウェブ・ハンドラー・コンポーネントは、インプット・コレクション・コーラブル、ドメイン・ロジック・コーラブル、レスポンス・ビルディング・コーラブルで構成されるAction記述を構築します。そして、Actionハンドラは、適切な順序でコーラブルを呼び出し、その過程で必要に応じてオブジェクトインスタンスを解決します。

Domain

ADRのDomainは、ドメインロジックへのエントリーポイントであることを覚えておいてください。ドメインロジック自体は、単一のインフラストラクチャの相互作用のような単純なものかもしれませんし、相互に接続されたサービスやオブジェクトの複雑な層で、最終的なステータスを返す前にお互いに通知したり観察したりするものかもしれません。

ActionResponderも、Domainの内部動作には関心がありません。ADRの関心事は、ActionDomainを呼び出し、ResponderDomainの結果を提示することだけです。

Domainには何を書く?

ADRはユーザーインターフェースのパターンであることを思い出してください。HTTPリクエストの読み取りに関係するものはすべてActionに入り、HTTPレスポンスの構築に関係するものはすべてResponderに入ります。 それ以外のすべてのものは、Domainに入れなければなりません。

簡単に覚えられるヒューリスティックな方法があります。"ストレージに触れるものは Domain に置く" ということです。ストレージとは、データベース、キャッシュ、ファイルシステム、ネットワークなど、あらゆるインフラや外部リソースを含みます。

では、ストレージとのやりとりが、ビジネスロジックを全く必要としないテンプレートテキストの翻訳など、プレゼンテーションの値のみを取得する場合はどうでしょうか?レスポンダがそのような値を自ら取得することは合理的かもしれません。

そうであっても、一貫したヒューリスティックな考え方のために、私は、Actionが(Domainの入力の一部として)Domainに、返されるペイロードの一部としてそれらの値を返す命令を渡すのが良いと考えています。

ユーザーインターフェースとの適切な分離

Domain_は、HTTP固有の依存関係から完全に分離する必要があります。ActionResponder のユーザーインターフェースのコードが Domain に依存するのは妥当ですが、 Domain のコードが特定のユーザーインターフェースに依存するのは 妥当ではありません

そのためのテストとして、Domainのエントリーポイントが、HTTPインターフェースではなく、コマンドラインインターフェースでどの程度動作するかを調べてみてください。答えが「簡単ではない」ということであれば、DomainはHTTPに依存しすぎていると考えられます。

Domainはどのように入力を受け取るべきでしょうか?

ドメイン層へのエントリーポイントであるDomainのシグネチャは、そのロジックがユーザの入力から必要とするものであれば何でも構いません。1つ以上の個別の引数(タイプヒントがあってもなくても)、Data Transfer Object、さらにはHTTPリクエストに含まれる全ての入力値が含まれた配列等です。受け取るべきではないものは、HTTPリクエストそのものです。

Domainは何を返すべきですか?

これは、ドメイン・ロジックの詳細と、アプリケーションがユーザーへの出力に何を要求するかによります。これは必然的にアプリケーションのコア部分に関わることであり、 ユーザーインターフェースに左右されるべきではありません。

Domainは、場合によっては何も返さないこともあります。また、単純な値や単一のオブジェクトを返す場合もあります。また、オブジェクトや値の複雑なコレクションを返す場合もあります。このような場合には、Domain Payloadを使用して、ユーザーインターフェースの境界を越えてドメインの結果や状態を伝達し、解釈することができます。

その後、Domainの結果をHTTPレスポンスとしてどのように表示するかは、Responderに任されています。

Responder

ResponderはどのようにしてHTTPレスポンスを生成するのですか?

Responderは、newやDIされたFactoryによって、自分自身のHTTPレスポンスオブジェクトを生成して返すことができます。

また、Responderは、コンストラクタのパラメータとして注入されたHTTPレスポンスオブジェクトを受け取ることもできます。Responderは、注入されたレスポンスオブジェクトを修正してから、それをActionに返すことができます。実際には、HTTPレスポンスオブジェクトが既存のシステム全体で共有されている場合、ウェブハンドラーが共有オブジェクトに既にアクセスしている可能性があるため、ResponderはHTTPレスポンスオブジェクトを返す必要がないかもしれません[1]。

これらのアプローチにはそれぞれトレードオフがありますが、Responderの実装としては有効です。

補足

[1]のnodejsの例: @koa/routerはコンテキスト上でHTTPレスポンスを構築するのに必要なパラメーターを設定するので"返す必要がない"パターンに該当します

Generic or Parent Responders

レスポンス構築のロジックが非常に単純であるため、1つのResponderがユーザーインターフェース全体のレスポンス構築作業をすべて行うことができる場合があります。同様に、基本機能を持つ親Responderを、機能を追加・変更した子Responderで拡張することもできます。

これらはどちらもADRの実装として受け入れられます。重要な点は、応答構築作業の単純さや複雑さではなく、そのような作業がActionDomainから完全に分離されていることです。

Templates and Transformations

Responderの中で、Template ViewTwo Step ViewTransform Viewの各実装を使用することは、HTTPレスポンスのコンテンツを構築する上で、全く問題ありません。

Widgets and Panels

プレゼンテーションの中には、異なるデータソースを持つ複数のパネル、コンテンツエリア、サブセクションがある場合があります。これらは、テンプレートシステムやその他のプレゼンテーションサブシステムで処理することができますが、Domainから独自のデータを取得してはいけません。代わりに、Actionがウィジェットを起動したときに、Domainがこれらのウィジェットに必要なデータをすべて提供する必要があります。

その方法の初歩的な例については、Solving The "Widget Problem" In ADRを参照してください。

補足

例えばHypertext Application Language (HAL)を正確に・効率的に構築する為のパッケージはResponderの中で利用されます

その他のTopics

Content Negotiation

コンテンツネゴシエーションは、HTTPレスポンスのボディコンテンツをどのように表示するかを扱うため、最も適切にはResponderに属します。例えば、Responderは、HTTP RequestのAcceptヘッダーに基づいて、HTTP Responseのボディのコンテンツタイプをネゴシエートすることができます。

しかし、HTTPリクエストが、ウェブハンドラー、ActionDomain(高価なリソースの使用を伴う可能性がある)を経て、最終的にResponderに到達し、唯一、Responderが許容されるコンテンツタイプのいずれも満たすことができないと判断するのは、非効率的である。

このように、リクエストをルーティングした後、ウェブハンドラーは、ルーティングされたActionResponderを先読みして、Responderがリクエストで許容されるコンテンツタイプのいずれかを提供できるかどうかを判断することが合理的であるかもしれません。

これはそれ自体がネゴシエーションではなく、単にAcceptsヘッダーにResponderが扱えると表明しているタイプが含まれているかどうかを確認するものです。WebハンドラーとResponderはすべてユーザーインターフェイス層に存在するので、これは不適切な依存関係を示すものではありません。

例としては、Radarフレームワークを参照してください。

  • デフォルトのResponderクラスでは、応答可能なコンテンツタイプを報告します (code).

  • The route class, Responder をルートに関連付ける (code).

  • 受け入れ可能かどうかをテストするrouter rule (code).

Sessions

上記の Domain のヒューリスティックな考え方を思い出してください。「ストレージに触れているものは Domain に属する」ということです。 セッションはストレージ(ファイルシステム、データベース、キャッシュなど)から読み書きを行います。したがって、セッションの作業はすべて Domain で行う必要があります。例えば、以下のようになります。

  • Actionは、入力された Session ID(もしあれば)を読み取って、それをDomainへの入力として渡すことができます。

  • その後、DomainはそのIDを使ってSession Storeの読み書きを行い(もしくは新規作成を行い)、後で結果の一部としてSession IDや関連データをDomain Payloadで返すことができます。

  • その後、Responderは、Domainの結果からSession IDとデータを読み取り、それに基づいてCookieを設定することができます。

しかし、Sessionの実装の中には、言語やフレームワークと密接に絡み合っていて、 純粋なDomainの作業には適さないものもあります。たとえば、PHP のセッション拡張機能は複数の問題を組み合わせています。

  • session_start() は、入力されたリクエストデータからSession ID を直接読み込み、次に $_SESSION というスーパーグローバルデータをストレージ自体から読み込みます。これにより、入力の収集とインフラとの相互作用に関する懸念が解消されます。

  • session_commit() は、$_SESSION のスーパーグローバルデータをストレージに書き戻し、クッキーヘッダを発信するレスポンスバッファに直接書き込みます。これにより,インフラとのインタラクションとプレゼンテーションの問題が解決されます.

このような状況では、HTTPリクエスト・オブジェクトによる自動入力収集プロセス、HTTPレスポンス・オブジェクトによる自動出力プロセス、インフラとドメイン・ロジックの懸念をインターセプトすることは困難です。

これはADR自体の問題ではなく、PHPのセッション関数がリクエストの読み込み、ストレージとの連動、レスポンスの送信をどのように組み合わせているかという問題です。 これが問題になるのは、PHP の自動化された動作にフックされていない HTTP リクエスト/レスポンスオブジェクトを使用する場合だけです。

これを回避する方法があるならば、そうすべきです。一つの方法は、自動セッション処理の一部の要素を無効にし、他の要素はそのままにすることです。また、自動セッション処理を完全に回避し、ここで紹介するようなのようなドメインロジックに適したソリューションを採用するという方法もあります。

残念ながら、入力の収集、ストレージからの読み書き、出力の提示をきれいに分離することが**必要ですが、言語やフレームワークの制約の中では、そうすることが現実的でない場合もあります。そのような場合には、HTTPリクエストとレスポンスオブジェクトを使ったセッション作業は、私たちが望むほどクリーンではない方法で行われなければならないかもしれません。

認証

セッションと同様に、認証作業は、どこかの時点でストレージに触れる可能性があるため、Domainで行うべきです。例えば、以下のようになります。

  • Action は、HTTP リクエストから認証情報 (トークンやセッション ID など) を収集し、それを入力として Domain に渡すことができます。

  • Domain はストレージシステムと連携して、認証情報の有効性や有効期限などを確認し、その認証情報に関連付けられているユーザー固有のデータを読み込むことができます。

  • 認証状態に基づいて、Domain は、匿名または無効なユーザに対しては早期に返答するか、または他のドメインロジックに進み、後で認証状態(またはその欠如)を結果の一部として、おそらく Domain Payload の一部として返すことができます。

  • Responderは、Domainの結果に含まれるユーザ情報を検査し、適切に表示することができます。

Routing

多くの場合、開発者はいくつかのエンドポイントを認証されたユーザーのみに制限したいと思うでしょう。それは、ユーザーインターフェースコンポーネントであるルーターが、認証ロジックが存在するドメイン層と対話しなければならないということでしょうか?

答えは、"そうではないかも"。 ルートの条件が「誰であるかに関わらず、ユーザーは全く認証されていないか」というようなものに基づいている場合、答えは認証ではなく匿名をチェックすることです。

つまり、受信したHTTPリクエストにクレデンシャルやトークンが関連付けられていなければ、そのリクエストは匿名であり、適切にルーティングすることができます。クレデンシャルの保存と検証を含む認証作業は、Routerから削除して Domain に置くことができます。

Applicability

認証はユーザーとのやりとりを識別して管理するものなので、ユーザーインターフェースのコードに含まれるべきだという主張があります。私は直感的にそうすることに反対ですが、それは支持できる視点です。

そこで問題となるのは、ドメイン・ロジックに対してユーザーをどのように表現するかということです。ドメイン・ロジックがユーザー・インターフェース・コードに依存することは避けたいので、ユーザー・コンポーネントはユーザー・インターフェース・レイヤーによって提供されてはなりません。

ここでの一つの解決策は、ユーザーインターフェースコードがドメイン層によって提供されるUserコンポーネントを作成することです。このUserコンポーネントは、コンストラクタのパラメータとして、あるいはHTTPリクエストの追加パラメータとして、あるいはその他の方法でActionに渡すことができます。そして、Actionは、UserコンポーネントをDomainへの入力として扱うことができ、そこからすべてが始まります。

このアプローチには問題があることは想像できますが、Domainの作業結果を提示するユーザーインターフェイスフレームワークの制約を考えると、それ以外の方法はあり得ないのかもしれません。

Authorization

認証はユーザーを特定するものですが、認可はそのユーザーが何をすることができるかを制御するものです。権限付与の作業は、間違いなく Domain に属します。

認証の場合と同様に、ユーザーが特定のルートに沿って派遣されることを許可されているかどうかをルータが知ることができない場合、ルーティング(ユーザーインターフェイスの問題)をどのように行うことができるでしょうか?その答えは、認証とは特定のルートに対するものではなく、 そのルートがつながる特定の Domain の機能に対するものだということです。また、ドメイン内の特定のリソースに関する機能も対象となる場合があります。その場合は、認可チェックの一環として、ドメイン内のリソースを(一部または全部)読み込む必要があります。

このように、ユーザーが特定の機能を実行することを許可されているかどうかをチェックするのは Domain です。許可されていれば、Domainのロジックは続行しますが、許可されていなければ、Domainは適切な報告を行います。これにより、ユーザーインターフェースコンポーネントではなく、 Domain がドメインレイヤーの機能に対する適切な権限を持つことになります。

これについての詳しい説明は、Auth Gates and User Rolesの項を参照してください。

Client-Side Use

ADRパターンは、クライアントサイドでの使用を想定していません。クライアント側には、オリジナルのModel View ControllerModel View Presenterなど、既存の優れたユーザーインターフェースパターンがたくさんあります。

Command Line Use

ADRは、サーバーサイドアプリケーションのユーザーインターフェースパターンとして想定されていますが、非対話型のコマンドラインアプリケーションのユーザーインターフェースパターンとしても機能します。つまり、コマンドが起動された瞬間に提供された値だけで完結するのであれば、次のようになります。

  • Actionは、コマンドラインから渡された引数、フラグ、オプションなどの入力を収集し、Domainを起動して結果を返し、Responderを起動して(もしあれば)出力を生成します。

  • Domainはそのままです。

  • Responderは、HTTPレスポンスを生成するのではなく、_ドメインの結果を使ってSTDOUTとSTDERRに書き込みます。

この場合、Domainは、STDOUTとSTDERRに書き込むロギングシステムやnotifier/observerシステムを使用して、ユーザーに継続的な出力を提供することができます。ロギングや通知はDomainの運用に付随するものなので、これは必ずしもADRに違反するものではありませんが、このパターンが必ずしも環境に適しているとは言えないことを示しています。

この種の非対話的な使用の一例として、Cadre CliAdrを参照してください。

ADRパターンは、コマンド起動後にユーザーの追加入力を必要とするインタラクティブなコマンドライン・アプリケーションではうまく機能しません。

補足

ADRパターンは、コマンド起動後にユーザーの追加入力を必要とするインタラクティブなコマンドライン・アプリケーションではうまく機能しません。

Client-Side Useでも書かれていますが、ADRは対話型のアプリケーションには適しません.

20
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
14