PatrickSJ
PatrickSJ

Reputation: 510

DDD, Status object / Value Objects

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?

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
    }
}

Upvotes: 0

Views: 2381

Answers (3)

Constantin Galbenu
Constantin Galbenu

Reputation: 17693

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.

Upvotes: 3

mgonzalezbaile
mgonzalezbaile

Reputation: 1086

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.

Upvotes: 1

poul_ko
poul_ko

Reputation: 347

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.

Upvotes: 0

Related Questions