0

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
    }
}
PatrickSJ
  • 510
  • 5
  • 13

3 Answers3

3

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.

Constantin Galbenu
  • 16,951
  • 3
  • 38
  • 54
1

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.

mgonzalezbaile
  • 1,056
  • 8
  • 10
0

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.

poul_ko
  • 347
  • 2
  • 6
  • I think I will go for the static (yes, it is fairly static) approach. How would you handle something that is defined by the end-user (and thus not as static) when you want to ensure a valid value is picked? Hard-coded as you suggest I'd just set `$proposal_status = ProposalStatusCode::PENDING_CUSTOMER_APPROVAL`, but if it is dynamic and thus needs to be looked up in an external source...? – PatrickSJ Dec 09 '17 at 04:04
  • I suggest to introduce a really-static-and-never-changes (hardcoded) ID for ProposalStatus. It should be used in business code. And also you need some code what implements translation from user-defined value to that hardcoded ID and back (look up logic). I prefer place it outside of business layer. – poul_ko Dec 09 '17 at 04:38
  • Sorry, I meant that for other statuses and types, if they aren't as static as this proposal status, then an enumeration might not really work. If that were the case, how then would an object (such as proposal w/ a proposal type) handle that sort of lookup? – PatrickSJ Dec 09 '17 at 05:14
  • A not static status/type/kind probably should be implemented as an entity with its own Id and other attributes (e.g. Code, Name, Description). Please describe a scenario where you need lookup logic for such non-static characteristic. – poul_ko Dec 09 '17 at 05:28
  • Scenario: A proposal has an overall type. If the proposal type has a std. questionnaire associated then the questionnaire needs to be part of the proposal as it will eventually be analyzed by an analysis class which will auto-generate the items of the proposal. Some proposals are free-form items, some have placeholder for items to be selected (configurable), and others auto-generate. – PatrickSJ Dec 09 '17 at 05:46
  • What is the problem in scenario described? ProposalType looks like an aggregate, StdQuestionnaire probably is a part of this aggregate. Items of a proposal probably is a part of Proposal aggregate. Proposal entity refers ProposalType by Id, analysis class can find ProposalType entity by this Id and look into its StdQuestionnaire. – poul_ko Dec 09 '17 at 06:04
  • Narrowing my question: "Proposal entity refers ProposalType by Id". So I need have a value object class that is just the ID right? Only how do I get that list of all possible value objects for each proposal_type? Is the ProposalTypeId class supposed to be auto-generated? – PatrickSJ Dec 09 '17 at 06:11
  • Implementation of entity Id is a metter of taste. It can be just integer (autogenrated by DB), GUID or string what never changes. You can wrap it into value object if you want. When you need all possible proposal types just call ProposalTypeRepo and get the data. Maybe I still can't understand your problem... Please ask new question or let's go to chat as SO suggests. – poul_ko Dec 09 '17 at 06:54