存在理由層
「すべてのものには、それが存在するための理由がある」
—ライプニッツ『充足理由律』(1714年)
存在の理由
ExpressDeliveryが速達配送として成り立つのは、速達配送の能力を持っているからです。StandardDeliveryが通常配送として成り立つのは、通常配送の能力を持っているからです。この「なぜその存在でいられるのか」の根拠が、raison d’être(レーゾンデートル:存在理由)です。
存在理由層は、このraison d’êtreを一つのオブジェクトとして表現する設計パターンです。
final readonly class ExpressDelivery
{
public Fee $fee;
public function __construct(
#[Input] OrderData $order, // 内在
#[Inject] ExpressShipping $reason // 存在理由
) {
$this->fee = $reason->calculateFee($order->weight);
}
}
ExpressShippingがExpressDeliveryのraison d’êtreです。速達配送に必要な道具一式をまとめて提供します。
$beingとしての存在理由
存在理由オブジェクトは$beingプロパティとしても使えます。このとき、そのオブジェクトの型が変容先の判別根拠になると同時に、その存在様式に固有のメソッドも提供します。
final readonly class ExpressDelivery
{
public Fee $fee;
public function __construct(
#[Input] OrderData $order,
#[Input] ExpressShipping $being // 型が変容先を決定し、速達固有のメソッドを提供
) {
$this->fee = $being->calculateFee($order->weight);
}
}
final readonly class StandardDelivery
{
public Fee $fee;
public function __construct(
#[Input] OrderData $order,
#[Input] StandardShipping $being // 型が変容先を決定し、通常配送固有のメソッドを提供
) {
$this->fee = $being->calculateFee($order->weight);
}
}
ExpressShipping $beingという型そのものが、なぜExpressDeliveryになるのかの理由です。フレームワークはこの型を読み取り、対応する変容先を自動選択します。
どのReasonオブジェクトも#[Inject](超越の能力を提供)にも$being(運命を決定)にもなれます。違いはオブジェクト自体ではなく、使われ方にあります。ある文脈で#[Inject]として患者を評価するJTASProtocolが、別の文脈では$beingとして運命を決定することもできます。
存在理由クラスの定義
存在理由クラスは、特定の存在様式を実現するために必要なサービスをまとめたものです:
namespace App\Reason;
final readonly class ExpressShipping
{
public function __construct(
private PriorityCarrier $carrier,
private RealTimeTracker $tracker,
) {}
public function calculateFee(Weight $weight): Fee // 速達料金
{
return $this->carrier->expressFee($weight);
}
public function guaranteeDeliveryBy(Address $address): \DateTimeImmutable // 配達日保証
{
return $this->carrier->guaranteedDate($address);
}
public function realTimeTrack(TrackingId $id): TrackingStatus // リアルタイム追跡
{
return $this->tracker->realTimeStatus($id);
}
}
final readonly class StandardShipping
{
public function __construct(
private RegularCarrier $carrier,
private BatchTracker $tracker,
) {}
public function calculateFee(Weight $weight): Fee // 通常料金
{
return $this->carrier->standardFee($weight);
}
public function estimateDeliveryWindow(Address $address): DateRange // 配達期間の見積もり
{
return $this->carrier->estimateWindow($address);
}
}
個別注入との違い
存在理由層は#[Inject]を使います。では複数の#[Inject]をバラバラに使う場合と何が違うのでしょうか。
個別の注入:
public function __construct(
#[Input] OrderData $order,
#[Inject] PriorityCarrier $carrier,
#[Inject] RealTimeTracker $tracker,
#[Inject] InsuranceService $insurance,
#[Inject] DeliveryScheduler $scheduler
) {
// バラバラの道具を個別に使用
}
存在理由層:
public function __construct(
#[Input] OrderData $order,
#[Inject] ExpressShipping $reason // 関連道具がまとまった存在理由
) {
$this->fee = $reason->calculateFee($order->weight);
}
「ExpressDeliveryになるには何が必要か?」という問いに、存在理由オブジェクト一つが答えます。オブジェクト自身は「何になるか」を宣言し、存在理由は「どうやってその状態になるか」を実現します。
Potentialを返すReason
ここまでのReasonはFeeのような即時の値を返していました。しかし注文処理を考えてみましょう。在庫確保・決済・配送手配のすべてが成功してからコミットする必要があります。決済が失敗したのに在庫だけ確保されたままでは困ります。
このような調整が必要な場面で、Reasonは値の代わりにPotentialを返します。Potentialは遅延操作を保持するオブジェクトで、後からbe()で実現されます。すべての参加者の準備が成功するまで、何もコミットされません。このパターンは、複数の外部操作をすべて準備してからコミットする必要があるときだけ使います。
Potential: 準備済み・未コミット
Reasonのメソッドは外部操作を準備し、Potentialを返します:
final class PaymentGateway
{
public function authorize(string $cardNumber, int $amount): PaymentCapture
{
$authCode = $this->api->authorize($cardNumber, $amount);
return new PaymentCapture(
$authCode,
$amount,
fn () => $this->api->capture($authCode, $amount),
);
}
}
PaymentCaptureはPotentialです。認証コードとキャプチャの遅延操作を保持しています。決済は認証済みですが、まだ確定していません。be()で確定します:
$capture = $gateway->authorize($cardNumber, $amount);
$capture->authorizationCode; // 即座に利用可能
$capture->be(); // キャプチャを確定
Moment: Potentialを保持する
ReasonからPotentialを受け取って保持するクラスをMoment(ヘーゲルの契機—全体の中でのみ意味を持つ不可欠な側面)と呼びます。MomentはMomentInterfaceを実装します。これはメソッドを1つだけ持つインターフェースで、フレームワーク本体ではなくアプリケーション側で定義します(注文処理デモ参照):
interface MomentInterface
{
public function be(): void;
}
final readonly class PaymentCompleted implements MomentInterface
{
public PaymentCapture $capture;
public function __construct(
#[CardNumber] public string $cardNumber, // DI qualifier attribute
#[Amount] public int $amount, // DI qualifier attribute
#[Inject] PaymentGateway $gateway,
) {
$this->capture = $gateway->authorize($cardNumber, $amount);
}
public function be(): void
{
$this->capture->be();
}
}
Momentは前段のオブジェクトからではなくDIコンテナによって生成されます。そのため値は#[Input]ではなく、モジュールで束縛したアプリケーション定義の修飾属性(#[CardNumber]、#[Amount])を通じて渡されます。
収束: FinalがMomentを実現する
複数のMomentがすべて揃う必要があるとき、Final Objectはそれらを#[Inject]で受け取り、各Momentのbe()を呼びます。これは外部からの命令ではなく、自己完成です:
final readonly class OrderConfirmed
{
public string $orderId;
public string $status;
public function __construct(
#[Inject] public InventoryReserved $inventory,
#[Inject] public PaymentCompleted $payment,
#[Inject] public ShippingArranged $shipping,
) {
$this->inventory->be();
$this->payment->be();
$this->shipping->be();
$this->orderId = 'ORD-' . date('Ymd') . '-' . bin2hex(random_bytes(4));
$this->status = 'confirmed';
}
}
いずれかのMomentが生成できなければ(Reasonが失敗したため)、Final Objectは構築されず、何もコミットされていません。準備段階には手動のロールバックフラグもネストされたtry-catchも不要です。すべてのMomentが存在すれば、be()が遅延操作を順にコミットします。後続のbe()が失敗した場合、先に成功したコミットは残ります。その補償はFinal Objectではなく、Reason(たとえばキャプチャを取り消せるゲートウェイ)の責務です。
このパターンを使う場面
複数の外部操作をすべて準備してからコミットする必要があるときに、Potentialを返すReasonを使います。外部サービスをまたぐ真のアトミック性は、サービス側(共有トランザクションや補償処理)に依存します。このパターンが保証するのは、準備に失敗したら何もコミットされないことです。Reasonが即時の値を返す単純なケースでは不要です。
存在できなかった、という結果もまた扱う必要があります。検証とエラーハンドリングでその扱い方を学びます ➡️