BeMart

G-25 — BDRはdomain noun + readonly propertyで表す

G-25 — BDRはdomain noun + readonly propertyで表す

Context

Ray.MediaQuery移行で、bool / int / string / conditional guard / trailing SELECT の戻り値を PostQueryInterface 実装クラスにした。

初期実装では GeneratedId, ProductCopyResult, BulkStatusUpdateResult のような機械的な名前や、changedCount() / exists() / value() のような単純getterを作った。これは動くが、BDRの値オブジェクトとしては余計な抽象と語彙の揺れを生む。

Problem

BDRクラスを「実装都合」や「結果であること」で命名すると、以下が起きる。

Solution

BDRは domain noun + final readonly + public property を基本形にする。

use Override;
use Ray\MediaQuery\Result\PostQueryContext;
use Ray\MediaQuery\Result\PostQueryInterface;

final readonly class ProductStatusUpdate implements PostQueryInterface
{
    public function __construct(public int $changedCount) {}

    #[Override]
    public static function fromContext(PostQueryContext $context): static
    {
        $row = $context->rows[0] ?? [];
        $changedCount = is_array($row)
            ? (int) ($row['changed_count'] ?? 0)
            : $context->statement->rowCount();

        return new static($changedCount);
    }
}

呼び出し側:

$update = $productCommand->bulkUpdateStatus($codes, $newStatus);
$changedCount = $update->changedCount;

Naming rules

避ける 代替
GeneratedId AllocatedId
ProductCopyResult CopiedProduct
BulkStatusUpdateResult ProductStatusUpdate
TrackingNumberResult ShippingTrackingNumber
PaymentVerifyResult PaymentVerification
PurchaseFlowResult PurchaseTotals
FooData domain noun
FooManager domain action / capability noun

原則:

Code example

final readonly class FavoritePresence implements PostQueryInterface
{
    public function __construct(public bool $exists) {}

    #[Override]
    public static function fromContext(PostQueryContext $context): static
    {
        return new static($context->rows !== []);
    }
}

final readonly class EmailUniqueness implements PostQueryInterface
{
    public function __construct(public bool $unique) {}

    #[Override]
    public static function fromContext(PostQueryContext $context): static
    {
        return new static($context->rows === []);
    }

    public function assertUnique(): void
    {
        if (! $this->unique) {
            throw new EmailAlreadyRegisteredException();
        }
    }
}

FavoritePresence::$exists はただの値なので property。EmailUniqueness::assertUnique() は例外を投げる domain guard なのでメソッドとして残す。

Anti-pattern

final class BulkStatusUpdateResult implements PostQueryInterface
{
    public function __construct(private readonly int $changedCount) {}

    public function changedCount(): int
    {
        return $this->changedCount;
    }
}

これは Result suffix + private property + getter になっていて、domain objectというより JavaBeans 風DTOになっている。

final readonly class GeneratedId implements PostQueryInterface
{
    public function __construct(public string $value) {}
}

GeneratedId も弱い。IDはほぼ常に生成されるため、「割り当て済み」「次に使う」「予約済み」などの domain semantics を名前に出す。

Where this matters

Where surfaced

Ray.MediaQuery全面移行で MediaQueryExecutor / internal proxy を撤去した後、BDRの初期名が GeneratedId, ProductCopyResult, BulkStatusUpdateResult などに揺れた。レビューで「GeneratedId は Data / Manager と同じ匂いがある」「単純getterより public readonly property が自然」と判明し、AllocatedId, CopiedProduct, ProductStatusUpdate, PaymentVerification, PurchaseTotals へ整理した。