Subscribe to PHP Freaks RSS

Modelling quantities - an exercise in designing value objects

syndicated from planet-php.net on March 27, 2018

I recently came across two interesting methods that were part of a bigger class that I had to redesign:



class OrderLine
{
    /**
     * @var float
     */
    private $quantityOrdered;

// ...

/** * @param float $quantity */ public function processDelivery($quantity) { $this->quantityDelivered += $quantity;

$this->quantityOpen = $this->quantityOrdered - $quantity; if ($this->quantityOpen < 0) { $this->quantityOpen = 0; } }

/** * @param float $quantity */ public function undoDelivery($quantity) { $this->quantityDelivered -= $quantity; if ($this->quantityDelivered < 0) { $this->quantityDelivered = 0; }

$this->quantityOpen += $quantity; if ($this->quantityOpen > $this->quantityOrdered) { $this->quantityOpen = $this->quantityOrdered; } } }


Of course, I've already cleaned up the code here to allow you to better understand it.



What happens here is: we have an order line, which keeps track how much of a certain product has been "ordered", and then how much of it has been "delivered" so far. It also keeps track of how much is currently still "open". Changes to these "delivered" and "open" quantities happens when we "process" a delivery, or "undo" a delivery.



I was reminded of a recent blog post by Nicolò Pignatelli where he quoted a question from another programming website. Adopted to the situation at hand:



Which variable type would you use for representing a quantity?



[ ] Integer



[ ] Float



[ ] String



It's a trick question, because all the answers are wrong. Nicolò advises not to use a primitive type value, but to design a value object that can represent a quantity. Nevertheless, in this code base it's floats everywhere.



I'm still modified-copycat-ing Nicolò's blog post here, since he's asking some really good questions that can guide your thinking about value objects:



  • Does it make sense to add or subtract two [quantities]? Maybe.
  • Does it make sense to multiply or divide two [quantities]. Don’t think so.
  • Does it make sense to allow negative [quantities]? Probably not.


You have to ask yourself these questions, since these properties would hold for a float, but not for quantities. Quantities can (sometimes) be added, but in the domain of selling and stock they should certainly not be divided (maybe multiplied against a tariff of some sorts, but then the result is not a quantity anymore). It also makes no sense to order a negative quantity, since that would be more like giving a quantity away.



This exposes quite nicely the need for modelling; the need for "codifying" the knowledge we have about quantities in our domain.



We may start out by wrapping these floats in a simple object called Quantity:



final class Quantity
{
    /**
     * @var float
     */
    private $quantity;

public function __construct(float $quantity) { $this->quantity = $quantity; } }


By the way, I find that it really helps to write unit tests along the way. You don't have to do pure TDD, but at least make tests part of your workflow. It'll be great, since every unit test method can help you document every one of your design criteria or decisions.



It's also a good idea to not just start re-implementing the algorithm you find in existing code, like the code above. Think about what's going on. Follow your intuition. For example, those if statements that end up "capping" certain quantities (to 0, or to the ordered quantity), are kind of iffy (pun intended).



Unraveling the meaning of these different quantities by thinking about them, and remembering something the domain expert said, I realized that:



  1. These quantities are related.
  2. These quantities aren't all of the same kind.
  3. The if statements hide some important domain knowledge.

The open quantity is what remains to be delivered. So it's the result of subtracting all the quantities that have been delivered from the initially ordered quantity. However, this doesn't take into consideration that we can "over-deliver", that is, we can deliver more than was ordered, and that's perfectly fine. That's why we need to cap the open quantity to 0: if we over-deliver, there's just nothing more that needs to be delivered, and we don't need to give something back. This explains the first if statement.



The other way around, if we undo a delivery,

Truncated by Planet PHP, read more at the original (another 6177 bytes)