(jp) =
この投稿では、大規模なコード ベースを個別のドメインに構造化する別のアプローチについて説明します。 「ドメイン」という名前は、一般的な DDD パラダイム、またはドメイン駆動設計に由来しています。
この投稿の多くの概念は DDD の原則に触発されていますが、ドメイン駆動設計に厳密に従っているわけではありません。 このコンテキストでは、「ドメイン」は「モジュール」と呼ばれることもあります。 「ドメイン」とは、単に関連するもののカテゴリを指します。それだけです。
また、このアプローチは特効薬ではないことに注意することも重要です。 Spatie では、特定のプロジェクトのニーズに基づいて、異なるプロジェクト構造を選択します。 あなたのプロジェクトは、今日レビューする内容に適していない可能性があります。
私たちの経験では、今日の原則は大規模なプロジェクトで最も有益です。
- 初期の開発期間が半年から 1 年以上で、その後数年間の保守と拡張が行われる長期実行プロジェクト。
- ビジネスを代表する約50から100のモデル。
- 機能を外部に公開する数百のルート。
# では、「ドメイン」とは何ですか?
以前にこの種の大規模なプロジェクトに取り組んだことがあれば、「ビジネス ロジック」は決して単なるものではないことをご存知でしょう。 1 もの。 多くの場合、開発中に、より大きなドメイン内で「サブシステム」を識別します。 つまり、コードで解決しようとしている問題の集まりです。
例を挙げると、ユーザー管理、在庫管理、請求、契約などです。 他にもたくさん考えられると思います。
ほとんどの場合、すべてのサブシステムには 1 つまたは複数のモデルがあります。 しかしそれだけではありません。モデルを操作したり、モデルでアクションを実行したり、システム固有の検証ルールやシステム間でデータを渡す方法などを設定したりできます。
標準の Laravel アプリケーションを見ると、1 つのシステムを記述するコードが複数のディレクトリに分散していることがよくあります。
app/
├── Enums/
│ ├── ContractDurationType.php
│ └── ContractType.php
├── Exceptions/
│ └── InvalidContractDate.php
├── Models/
│ └── Contract.php
└── Rules/
├── ContractAvailabilityRule.php
└── ContractDurationRule.php
この構造は、私がより良い解決策を探すようになった最初の闘争でした。 1 つのこと、1 つのシステムを動作させるために、複数の場所を検索することがよくありました。
では、サブシステムをグループ化してみませんか? 次のようになります。
Domain/
├── Contracts/
├── Invoicing/
└── Users/
名前が見えます Domain
ここ。 Oxford Dictionary によると、「ドメイン」は次のように記述できます。
活動または知識の特定の領域。
活動の範囲、ドメインに基づいてコードをグループ化しています。 1 つの特定のドメイン フォルダーにズームしてみましょう。
Contracts/
├── Actions/
├── Enums/
├── Exceptions/
├── Models/
├── Rules/
├── Status/
└── ValueObjects/
最近の PHP 開発者は、これらのフォルダー名のほとんどをよく知っているでしょう。 もう少し注目に値するものもありますが。
# 行動
アクションは、このセットアップ全体でおそらく最も強力なツールです。 アクションは、そのドメイン内で操作を実行するクラスです。 これは、モデルの作成や更新などの単純なタスクの場合もあれば、契約の承認のような 1 つまたは複数のビジネス ルールに従うより複雑なタスクの場合もあります。
1 つのアクションは 1 つのタスクにのみ焦点を合わせているため、非常に柔軟です。アクションは他のアクションから構成することができ、必要な場所に注入することができます。
2 つのアクションが連携して動作する例を次に示します。 CreateOrUpdateContractLine
と ResolveContractLines
. 最初のものは、その名前が示すように、単一の契約行を作成または更新します。 2 つ目は、ユーザー入力のコレクションをループし、行を 1 つずつ解決します。
これは何ですか ResolveContractLines
しましょう:
- ユーザー入力をループして、既存の行を作成または更新します。
- 現在契約に追加されている契約明細のリストを保持します。
- もう存在しないすべての行を削除します。ユーザーがそれらを削除しました。
コードは次のとおりです。
class ResolveContractLines
public function __construct(
CreateOrUpdateContractLine $createOrUpdateContractLine,
RemoveContractLine $removeContractLine
)
public function execute(
Contract $contract,
ContractLinesCollection $contractLinesCollection
)
$lineIds = [];
foreach ($contractLinesCollection as $contractLineData)
$contractLine = $this->createOrUpdateContractLine
->execute($contractLineData);
$lineIds[] = $contractLine->id;
$contractLinesToRemove = ContractLine::query()
->whereContract($contract)
->whereNotIn('id', $lineIds)
->get();
foreach ($contractLinesToRemove as $contractLine)
$this->removeContractLine->execute($contractLine);
アクションを一緒に構成するだけでなく、テストにも最適です。 アクションはサイズが小さく、責任が 1 つであるため、単体テストを非常に効率的に行うことができます。
アクションはまた、アプリのビジネス ロジックのほとんどをカプセル化します。コントラクト番号の生成、ステータスの変更、明示的な方法での副作用の処理などです。これにより、ほとんどのビジネスがカプセル化されているため、開発者はアプリケーションが何をするかを簡単に理解できるようになります。アクションとして。
DDD に興味がある場合は、おそらくコマンドについて考えているでしょう。 アクションはそれらの単純なバージョンです。 コマンド バスはなく、アクションは値を直接返す場合があります。 私たちのプロジェクトの範囲では、これは非常に扱いやすいアプローチです。
# 値オブジェクト
おそらく、このドメインがコントローラーや CLI コマンドとどのように結びついているのか疑問に思っているでしょう。 もちろん使う場所です。 ただし、理解する必要があるもう 1 つの抽象化があります。値オブジェクトです。
アップデート: このブログ記事を書いてから、「値オブジェクト」の名前について興味深い議論がありました。 名前を「データ転送オブジェクト」に変更しました。 このネーミングの詳細については、こちらをご覧ください。
気づいたか ContractLinesCollection
に渡された ResolveContractLines
前の例のアクション? それが値オブジェクトです。
ユーザー入力の操作は、必ずしも簡単ではありません。 たとえば、Laravel アプリケーションでは、フォーム データの配列または CLI 引数の配列を取得しますが、残りはあなた次第です。
値オブジェクトは、構造化された方法でそのユーザー データを表現したものです。 アクションを入力検証に関係させたくないので、値オブジェクトを渡します。 値オブジェクトに適用されるルールが 1 つあります。値オブジェクトが存在する場合、それらは有効です。
ほとんどの場合、値オブジェクトは、検証済みの要求データとアクションで使用できるプロパティとの間の単純なマッピングです。
値オブジェクトの例を次に示します。
class ContractLineData
public $price;
public $dateFrom;
public $dateTo;
public $article;
public static function fromArray(
array $input
): ContractLineData
return new self(
$input['price'],
Carbon::make($input['date_from']),
Carbon::make($input['date_to']),
Article::find($input['article_id'])
);
public function __construct(
int $price,
Carbon $dateFrom,
Carbon $dateTo,
Article $article
)
便宜上、パブリック プロパティを使用しています。 PHP で厳密に型指定された読み取り専用のプロパティが期待される理由は想像できるでしょう。
値オブジェクトを使用すると、アクションは実際のアクションのみに集中することができ、入力が有効かどうかを気にする必要がなくなります。 さらに、値オブジェクトを簡単に偽造できるため、テストがさらに簡単になります。
#つなぎ合わせ
ここまで、コントローラーや CLI コマンド、およびそれらがこの図にどのように適合するかについて、ほとんど何も説明してきませんでした。 それは意図的なものです。
ドメインが別々の領域に分割されているため、コントローラーやビューを 1 つも作成することなく、ドメイン全体を開発できます。 ドメイン内のすべてを簡単にテストでき、ほとんどすべてのドメインを他のドメインと並行して開発できます。
大規模なプロジェクトでは、これは非常に効率的なアプローチです。 1 つのプロジェクトに 2 人か 3 人のバックエンド デベロッパーが取り組んでおり、それぞれが隣り合って取り組んでいるドメインを持っています。
また、すべてのドメインがテストされているため、単一のフォームと統合テストを作成する前に、クライアントが必要とするすべてのビジネス ロジックが意図したとおりに機能することを確信しています。
ドメインが完成したら、それを消費できます。 ドメイン自体は、いつ、どこで使用されるかを気にせず、その使用ルールは外部に対して明確です。
これは、既存のドメインを使用して、1 つまたは複数のアプリケーションを構築できることを意味します。 私たちのプロジェクトの 1 つには、管理 HTTP アプリケーションと REST API があります。 どちらも同じドメインを使用しています。 アクション、モデル、ルールなど。このアプローチが開発中に効率的であるだけでなく、より優れたスケーリングを可能にすることがわかります。
管理 HTTP アプリケーションのコントローラーがどのように見えるかの例を次に示します。
class ContractsController
public function index()
public function edit(Contract $contract)
public function update(
Contract $contract,
UpdateContract $updateContract,
UpdateContractRequest $updateContractRequest
)
$contract = $updateContract->execute(
$contract,
ContractData::fromRequest($updateContractRequest)
);
return new ContractViewModel($contract);
ほとんどすべてのコントローラ アクションは次のように単純です。
- 要求データを検証し、値オブジェクトに解析します。
- アクションを実行します。この時点で下で何が起こるかはもう気にしません。
- この場合はビューモデルを使用して結果を返します。
# 最後に
ドメインでコードを構造化すると、単一プロジェクトの開発者間の効率が向上します。 さらに、サブシステムが分離され、十分にテストされているため、メンテナンスの複雑さが軽減されます。
アクションと値オブジェクトを使用することで、制御されたテスト可能な方法でドメインと通信できます。 最初の作成には時間がかかりますが、このアプローチは開発の初期段階であってもすぐに効果を発揮します。
このようにコードを構造化する最も重要な理由は、おそらく、理解しやすいからです。 私たち人間は、「モデル」、「アクション」、「ルール」などの抽象的なものでは考えません。 複雑なビジネス プロセスをサブシステムに分類します。 「契約」や「請求書」など。
私はこのような複雑なコード ベースを 2 年間構造化してきましたが、経験から言えば、今ではそれらについて推論するのがはるかに簡単になっています。 最終的に、開発者の経験は成功するための理論的知識やパラダイムと同様に重要であると私は信じています。
こんにちは、読んでくれてありがとう! この投稿が何らかの形で役立つことを願っています。
このトピックについてもっと話したい場合は、いつでもツイートまたは電子メールを送ってください。 これが私の ツイッター、そしてここに私の電子メールがあります。