DDD,状态对象/值对象

I've a class, Proposal, which has a $status of type ProposalStatus. Now, for the most part the ProposalStatus's identity does not change... but it does (sort of). It's $id is fixed, but the $display_name and $definition (just strings) can change over a long period of time as the master data is updated, but it will NOT change within the lifetime of an HTTP Request-Response.

Question #1 - Entity or Value Object?

Is a value object something is never supposed to change or only never supposed to change over the lifetime of a specific execution of the application? If the display name or definition are changed then really I expect / want it to be changed for everyone. However, since they can be defined outside of the proposal I'm think that just straight up makes them entities instead of value objects.

At no time does the Proposal change the ProposalStatus's attributes, it only changes which ProposalStatus is has.

Question #2 - How to set the status correctly for a domain-driven design?

My proposal object has the ability to manage it's statuses, but in order to do that I need to have a specific ProposalStatus object. Only, where is the list of statuses that allows it to returns the right expected to be?

  • I could get it from a the ProposalRepository... but everything is accessed via the aggregate root which the Proposal so that doesn't make sense.
  • I could have constants that match the $id of the ProposalStatus, but that seems wrong.
  • I could make a ProposalStatusRepository... but should I be accessing another repository from within the Proposal?
  • I could make a array of all possible statuses with the $id as the key and add to the proposal, but that isn't much different from a repository...

Example:

class ProposalStatus {
    protected $id; // E.g., pending_customer_approval
    protected $display_name; // E.g., Pending Customer Approval
    protected $definition; // E.g., The proposal needs to be approved by the customer
}

class Proposal {
    /**
     * The current status of the proposal
     * @var ProposalStatus
     */
    protected $proposal_status;

    public function withdraw() {
        // verify status is not closed or canceled
        // change status to draft
    }

    public function submit() {
        // verify status is draft
        // change status to pending customer approval
    }

    public function approve() {
        // verify status is pending customer approval
        // change status to approved
    }

    public function reject() {
        // verify status is pending customer approval
        // change status to rejected
    }

    public function close() {
        // verify status is not canceled
        // change status to closed
    }

    public function cancel() {
        // verify status is not closed
        // change status to canceled
    }
}

Is list of all possible proposal statuses static? I think it is. So ProposalStatus looks like a simple enumeration. Attributes like DisplayName and Definition are not related to business code.

Just define ProposalStatus as enumeration (static class with read-only fields or any other structure supported by your language). It shuld be defined in business layer. Bussiness code should be able to distinguish enumeration values (e.g. if (proposal.Status == ProposalStatus.Pending) { poposal.Status = ProposalStatus.Approved; }).

In application or even presentation layer define a dictionary that contains DisplayName and Definition mapped to ProposalStatus. It will be used only when displaying data to users.

In case that your ProposalStatus is a fixed list of values just go for the enumeration approach.

Otherwise you need to treat ProposalStatus as an AggregateRoot that users can create, update and delete (I guess). When assigning a ProposalStatus to a Proposal you just need the ID. If you want to check that the given ID exists you just need to satisfy the invariant with a specialized query. Specification pattern fits well here.

class ProposalStatusExistsSpecification
{
    public function isSatisfiedBy(string $proposalSatusId): bool
    {
        //database query to see if the given ID exists
    }
}

You can find here the Interfaces to implement your specification.

From what I understand from your domain, ProposalStatus should be a Value object. So, it should be made immutable and contain specific behavior. In your case, the behavior is testing for a specific value and initializing only to permitted range of values. You could use a PHP class, with a private constructor and static factory methods.

/**
 * ProposalStatus is a Value Object
 */
class ProposalStatus
{
    private const DRAFT                     = 1;
    private const PENDING_CUSTOMER_APPROVAL = 2;
    private const CANCELLED                 = 3;
    private const CLOSED                    = 4;

    /** @var int */
    private $primitiveStatus;

    private function __construct(int $primitiveStatus)
    {
        $this->primitiveStatus = $primitiveStatus;
    }

    private function equals(self $another): bool
    {
        return $this->primitiveStatus === $another->primitiveStatus;
    }

    public static function draft(): self
    {
        return new static(self::DRAFT);
    }

    public function isDraft(): bool
    {
        return $this->equals(static::draft());
    }

    public static function pendingCustomerApproval(): self
    {
        return new static(self::PENDING_CUSTOMER_APPROVAL);
    }

    public function isPendingCustomerApproval(): bool
    {
        return $this->equals(static::pendingCustomerApproval());
    }

    public static function cancelled(): self
    {
        return new static(static::CANCELLED);
    }

    public function isCancelled(): bool
    {
        return $this->equals(static::cancelled());
    }

    public static function closed(): self
    {
        return new static(static::CLOSED);
    }

    public function isClosed(): bool
    {
        return $this->equals(static::closed());
    }
}

class Proposal
{
    /** @var ProposalStatus */
    private $status;

    public function __construct()
    {
        $this->status = ProposalStatus::draft();
    }

    public function withdraw()
    {
        if (!$this->status->isClosed() && !$this->status->isCancelled()) {
            $this->status = ProposalStatus::draft();
        }
    }

    // and so on...
}

Note that immutability is an important characteristic of a Value object.