(jp) =
タイプセーフで透過的な方法でデータを操作できるようになったので、データを使って何かを始める必要があります。
データでいっぱいのランダムな配列を操作したくないのと同じように、プロジェクトの最も重要な部分であるビジネス機能をランダムな関数やクラス全体に分散させたくありません。
以下に例を示します。プロジェクトのユーザー ストーリーの 1 つは、「管理者が請求書を作成する」ためのものである可能性があります。 これは、請求書をデータベースに保存することを意味しますが、さらに多くのことを意味します。
- 最初に、個々の請求明細行の価格と合計価格を計算します。
- 請求書をデータベースに保存する
- 支払いプロバイダー経由で支払いを作成する
- すべての関連情報を含む PDF を作成する
- この PDF をお客様に送信します
Laravel での一般的な方法は、このすべての機能を処理する「ファット モデル」を作成することです。 この章では、この動作をコードベースに追加する別のアプローチを見ていきます。
モデルやコントローラーに機能を混在させる代わりに、これらのユーザー ストーリーをプロジェクトの第一級市民として扱います。 私はこれらを「アクション」と呼んでいます。
# 用語
それらの使用法を見る前に、アクションがどのように構造化されているかを議論する必要があります。 まず、彼らはドメインに住んでいます。
第二に、それらは抽象化やインターフェースのない単純なクラスです。 アクションは、入力を受け取り、何かを実行し、出力を提供するクラスです。 そのため、アクションには通常、パブリック メソッドが 1 つしかなく、コンストラクターが含まれることもあります。
プロジェクトの慣習として、すべてのクラスに接尾辞を付けることにしました。 確かに CreateInvoice
いいように聞こえますが、数百または数千のクラスを扱うとすぐに、名前の衝突が発生しないようにする必要があります。 分かりますか、 CreateInvoice
は、呼び出し可能なコントローラー、コマンド、ジョブ、または要求の名前でもあります。 できるだけ混乱をなくしたいので、 CreateInvoiceAction
が名前になります。
これは明らかに、クラス名が長くなることを意味します。 現実には、大規模なプロジェクトに取り組んでいる場合、混乱を避けるために長い名前を選択することは避けられません。 これは私たちのプロジェクトの 1 つからの極端な例です。冗談ではありません。 CreateOrUpdateHabitantContractUnitPackageAction
.
最初はこの名前が嫌いでした。 私たちは必死に短いものを考え出そうとしました。 最終的には、クラスの内容を明確にすることが最も重要であることを認めなければなりませんでした。 いずれにせよ、IDE のオートコンプリートは、長い名前の不都合を処理します。
クラス名が決まれば、次に克服すべきハードルは、アクションを使用するパブリック メソッドに名前を付けることです。 1 つのオプションは、次のように呼び出し可能にすることです。
class CreateInvoiceAction
public function __invoke(InvoiceData $invoiceData): Invoice
ただし、このアプローチには実際的な問題があります。 この章の後半で、他のアクションからアクションを構成する方法と、それがいかに強力なパターンであるかについて説明します。 次のようになります。
class CreateInvoiceAction
private $createInvoiceLineAction;
public function __construct(
CreateInvoiceLineAction $createInvoiceLineAction
)
public function __invoke(InvoiceData $invoiceData): Invoice
foreach ($invoiceData->lines as $lineData)
$invoice->addLine(
($this->createInvoiceLineAction)($lineData)
);
問題を特定できますか? PHP は代わりにクラス メソッドを探しているため、呼び出し可能オブジェクトがクラス プロパティである場合、PHP は呼び出し可能オブジェクトを直接呼び出すことを許可しません。 そのため、アクションを呼び出す前にアクションを括弧で囲む必要があります。
これは小さな不都合にすぎませんが、PhpStorm には別の問題があります。この方法でアクションを呼び出すと、パラメーターのオートコンプリートを提供できません。 個人的には、適切な IDE の使用はプロジェクトの開発の不可欠な部分であり、無視すべきではないと考えています。 そのため、現時点では、私たちのチームはアクションを呼び出し可能にしないことにしました。
別のオプションは、使用することです handle
、このような場合にデフォルト名としてLaravelでよく使用されます。 特にLaravelがそれを使用しているため、ここでも問題があります。
Laravelが使用を許可するときはいつでも handle
、例えば。 依存関係コンテナーからのメソッド注入も提供します。 私たちのアクションでは、コンストラクターに DI 機能を持たせたいだけです。 この章の後半で、この背後にある理由を詳しく見ていきます。
そう handle
も出ています。 アクションを使い始めたとき、実際にこのネーミングの難問についてかなり考えました。 結局、私たちは落ち着きました execute
. ただし、独自の命名規則を思い付くのは自由であることを覚えておいてください。ここでのポイントは、アクションの名前よりも、アクションの使用パターンに関するものです。
# 実践へ
すべての用語について説明したところで、アクションがなぜ役立つのか、実際にどのように使用するのかについて説明しましょう。
まず、再利用性について話しましょう。 アクションを使用する際の秘訣は、いくつかのものを再利用できるように十分に小さく分割し、過負荷にならないように十分な大きさに保つことです。 請求書の例を見てみましょう。請求書から PDF を生成することは、アプリケーションのいくつかのコンテキストで発生する可能性が高いものです。 確かに、請求書が実際に作成されたときに生成される PDF がありますが、管理者は、送信する前にプレビューまたは下書きを確認することもできます。
「請求書の作成」と「請求書のプレビュー」という 2 つのユーザー ストーリーには、明らかに 2 つのエントリ ポイントと 2 つのコントローラーが必要です。 一方、請求書に基づいて PDF を生成することは、どちらの場合も行われます。
アプリケーションが実際に何をするかを考え始めると、再利用できるアクションがたくさんあることに気付くでしょう。 もちろん、コードを抽象化しすぎないように注意する必要もあります。 時期尚早の抽象化を行うよりも、小さなコードをコピーして貼り付ける方がよい場合がよくあります。
経験則として、抽象化を行うときは、コードの技術的特性ではなく、機能について考えることをお勧めします。 2 つのアクションが似たようなことをする可能性がありますが、それらはまったく異なるコンテキストでそれを行いますが、あまりにも早く抽象化を開始しないように注意する必要があります。
一方、抽象化が役立つ場合もあります。 請求書 PDF の例をもう一度見てみましょう。請求書以外の PDF を生成する必要がある可能性があります。少なくとも、私たちのプロジェクトではそうです。 将軍を持つことは理にかなっているかもしれません GeneratePdfAction
、インターフェイスで動作することができます Invoice
次に実装します。
しかし、正直に言うと、私たちのアクションの大部分はユーザー ストーリーに特化したものであり、再利用できない可能性があります。 このような場合、アクションは不要なオーバーヘッドであると考えるかもしれません。 ただし、再利用性がそれらを使用する唯一の理由ではないため、しばらくお待ちください。 実際、最も重要な理由は、技術的な利点とはまったく関係ありません。つまり、アクションによって、プログラマーはコードではなく現実世界に近い方法で考えることができます。
請求書の作成方法を変更する必要があるとします。 典型的な Laravel アプリケーションでは、おそらくこの請求書作成ロジックがコントローラーとモデル、PDF を生成するジョブ、最後に請求書メールを送信するイベント リスナーに分散されています。 知っておくべき場所がたくさんあります。 繰り返しになりますが、コードはコードベース全体に広がり、意味ではなく技術的特性によってグループ化されています。
アクションは、そのようなシステムによって導入される認知負荷を軽減します。 請求書の作成方法に取り組む必要がある場合は、単純にアクション クラスに移動して、そこから開始できます。
誤解しないでください: アクションは、たとえば、 非同期ジョブとイベント リスナー。 ただし、これらのジョブとリスナーは、アクションが機能するためのインフラストラクチャを提供するだけであり、ビジネス ロジック自体は提供しません。 これは、ドメイン層とアプリケーション層を分割する必要がある理由の良い例です。それぞれに独自の目的があります。
再利用性と認知負荷の軽減が得られましたが、それ以上のものがあります!
アクションは、ほぼ単独で動作するソフトウェアの小さな断片であるため、単体テストは非常に簡単です。 テストでは、偽の HTTP リクエストの送信、ファサード フェイクの設定などについて心配する必要はありません。単に新しいアクションを作成し、モックの依存関係を提供し、必要な入力データを渡し、その出力でアサーションを行うことができます。
たとえば、 CreateInvoiceLineAction
: 請求する商品、金額、期間に関するデータが必要です。 合計価格と VAT を含む価格と含まない価格を計算します。 これらは、堅牢でありながらシンプルな単体テストを作成できるものです。
すべてのアクションが適切に単体テストされていれば、アプリケーションが提供する必要のある機能の大部分が実際に意図したとおりに機能することを確信できます。 あとは、これらのアクションをエンド ユーザーにとって意味のある方法で使用し、それらの部分の統合テストを作成するだけです。
# アクションの作成
前に簡単に説明したアクションの重要な特徴の 1 つは、依存性注入の使用方法です。 コンストラクターを使用してコンテナーからデータを渡しているため、 execute
コンテキスト関連のデータを渡すメソッド。 アクションからアクションからアクションからアクションを自由に構成できます…
あなたはアイデアを得る。 ただし、深い依存関係チェーンは避けたいものであることを明確にしましょう — コードが複雑になり、相互依存性が高くなります — それでも、DI を持つことが非常に有益な場合がいくつかあります。
の例をもう一度見てみましょう。 CreateInvoiceLineAction
VAT価格を計算する必要があります。 コンテキストに応じて、請求明細行の価格に VAT が含まれている場合と含まれていない場合があります。 VAT 価格の計算は些細なことですが、 CreateInvoiceLineAction
その詳細に関心を持つこと。
単純なものがあると想像してください VatCalculator
クラス — これは、 \Support
名前空間 — 次のように注入できます。
class CreateInvoiceLineAction
private $vatCalculator;
public function __construct(VatCalculator $vatCalculator)
$this->vatCalculator = $vatCalculator;
public function execute(
InvoiceLineData $invoiceLineData
): InvoiceLine
そして、次のように使用します。
public function execute(
InvoiceLineData $invoiceLineData
): InvoiceLine
$item = $invoiceLineData->item;
if ($item->vatIncluded())
[$priceIncVat, $priceExclVat] =
$this->vatCalculator->vatIncluded(
$item->getPrice(),
$item->getVatPercentage()
);
else
[$priceIncVat, $priceExclVat] =
$this->vatCalculator->vatExcluded(
$item->getPrice(),
$item->getVatPercentage()
);
$amount = $invoiceLineData->item_amount;
return new InvoiceLine([
'item_price' => $item->getPrice(),
'total_price' => $amount * $priceIncVat,
'total_price_excluding_vat' => $amount * $priceExclVat,
]);
の CreateInvoiceLineAction
順番に注入されます CreateInvoiceAction
. そして、これにも他の依存関係があります。 CreatePdfAction
と SendMailAction
、 例えば。
コンポジションが個々のアクションを小さく保ちながら、複雑なビジネス機能を明確で保守可能な方法でコーディングできるようにする方法を理解できます。
# アクションの代替案
この時点で言及する必要がある 2 つのパラダイムがあります。アクションのような概念を必要としない 2 つの方法です。
最初のものは、DDD に精通している人々には知られています: コマンドとハンドラーです。 アクションはそれらの単純化されたバージョンです。 コマンドとハンドラーは、何が必要か、どのように発生する必要があるかを区別しますが、アクションは、これら 2 つの責任を 1 つにまとめます。 コマンド バスがアクションよりも柔軟性を提供することは事実です。 一方で、より多くのコードを記述する必要もあります。
私たちのプロジェクトの範囲では、アクションをコマンドとハンドラーに分割することは、やり過ぎでした。 追加の柔軟性が必要になることはほとんどありませんが、コードを記述するのにはるかに長い時間がかかります.
言及する価値のある 2 番目の代替案は、イベント駆動型システムです。 イベント ドリブン システムで作業したことがある場合は、アクションが実際に使用される場所に直接結び付けられすぎていると考えるかもしれません。 繰り返しますが、同じ議論が当てはまります。イベント ドリブン システムはより柔軟性がありますが、私たちのプロジェクトではそれらを使用するのはやり過ぎでした。 さらに、イベント ドリブン システムは間接的なレイヤーを追加するため、コードがより複雑になり、推論が難しくなります。 この間接性は確かにメリットをもたらしますが、メンテナンスのコストを上回ることはありません。
すべてを把握し、すべての Laravel プロジェクトに最適なソリューションを用意していると言っているのではありません。 私たちはしません。 このシリーズを読み進めるときは、プロジェクトの特定のニーズに注意を払うことが重要です。 ここで提案されているいくつかの概念を使用できる場合もありますが、特定の側面を解決するために他のソリューションが必要になる場合もあります。
アクションは適切な柔軟性と再利用性を提供し、認知負荷を大幅に軽減するため、私たちにとって正しい選択です。 それらはアプリケーションの本質をカプセル化します。 実際、それらは、DTO およびモデルと共に、プロジェクトの真のコアと考えることができます。
次の章、コアの最後の部分であるモデルに進みます。