Categories
Architecture Software

The Value of “Value Objects”

Herberto Graça recently wrote a great summary on Value Objects, and I commented on it on Reddit, saying:

…something that could flesh out the case for value objects [is] the fact that it allows you to model complex rule interactions by representing the concepts of the core domain and letting them interact as they would in said setting. This results in code that reads very much like "business-speak", making it easier to translate back and forth from the stakeholders to the developers.

The comment has its own illustration of the idea, but I wanted to expand on the utility of value objects in a bit more depth in this blog post. If you need a primer on what value objects are in the first place, Graça’s article is a great start.

A lightning-fast DDD reminder

Within Domain Driven Design, we separate our code into at least two distinct models:

  • The Application Model, which crystalises the top-level use-cases (or commands) of the system (e.g. ApplyForFinancialLoan, ApproveLoanApplication, etc…)
  • The Domain Model, which represents the underlying, collaborating concepts that make up the use case (e.g. LoanApplication, Money LoanApplicationStatus, etc…)

Value objects form a subset of the Domain Model. They allow us to express business ideas and business rules in the codebase in a way that aligns more closely with how the experts of the domain would reason and talk about these concepts. In the case above, the business experts would be lenders, accountants and loan underwriters, for example.

But why care about that at all? Surely getting software that runs and does what it says on the tin is what matters at the end of the day, right?

Complexity, change, and business requirements

Well…we expect software to be able to change — for the better! — over time. It’s implied by the name there: software. One of its conceptual advantages over hardware is that it is maleable from a deployment perspective, so we can iteratively improve it when there are new requirements. If hardware needs upgrading, you need to invest a lot in managing the physical infrastructure to allow that change. Whether it is replacing the battery on a phone, or adding an extension to a building, it will still be more difficult to do than simply downloading and installing the latest version of a piece of software.

When the software is radically misaligned in its language — and clarity — from the domain experts, change becomes manifestly more difficult.

  1. Developers have difficulty in understanding what the business actually needs, and so invent new, developer-centric concepts in the codebase to overcome this knowledge-gap. This leads to an increase in the accidental complexity of the system.
  2. Businesses fails to understand the extent of the mis-match, and hence fail to understand why certain changes to the systems are difficult, time consuming or even outright impractical to make. They end up making poorer estimations on their roadmaps for delivering features to customers and struggle to keep a good cadance for improving their product offerings.

Business concepts can be quite complex, but they’re also somewhat irreducible. Complexity in software can be managed — or mitigated — by sound design decisions, but if the fundamental concepts that you’re being asked to implement or represent are complex, then you can’t really avoid them in the code. It has to exist somewhere.

So it is crucial to ensure that this irreducible business complexity is separated from the other types of complexity that can arise from your software (e.g. details over how it is delivered over the web). Part of that is through having a good architecture, but part of that is also through a representation of the business domain that maps well to what the domain experts are communicating.

We want to build up to the business complexity by expressing the simpler concepts in a rock-solid, isolated way. Then we can use those concepts as building blocks to codify the more complicated ideas.

Value objects are some of those crucial building blocks.

Worked example: parking in a shopping center

A local shopping center is introducing variable pricing for its parking spaces. It wants to encourage greener vechicles to be used in the area, so it will to provide discounts for more eco-friendly vechicles. It would also like to support its elderly community by providing discounts based on age. Furthermore, the cost of parking will be time-dependent – different periods of the day will be cheaper/more expensive.

The basic question to answer for any given customer is:

Given that I am Y years old and parked my vehicle V at time T, how much money should I be charged (C)?

Charge Rules

  • There is a basic charge of £1.40 per hour.
  • Between 09:00 and 18:30 Monday to Friday (weekday hours), the basic charge applies.
  • Between 18:30 and 22:00 Monday to Friday (evening hours), the basic charge gets a 20% reduction.
  • Between 09:00 and 22:00 Saturday and Sunday (weekend hours), the basic charge gets a 25% reduction.
  • Outside of these times, a £3 surcharge is applied.
  • Diesel vehicles pay an extra £0.40 per hour (independent of the time)
  • Petrol vehicles pay an extra £0.20 per hour (independent of the time)
  • Electric vehicles pay no extra cost
  • Customers that are aged 60+ get a £1 discount (independent of the time)

What is crucial though, is that those time periods are something that the shopping center want to be able to vary at different times of the year (e.g. with earlier opening/closing times). So those periods should be things that are easy to change (but the logic for calculating the charges would still be the same).

A possible solution

Here is how an implementation of the calculator could look (in PHP), assuming that we had value objects to encapsulate the meaning of specific terms:

final class CalculateParkingCharge implements Command
{
    private Clock $clock;
    private ParkingCharge $basicCharge;
    private Currency $currency;
    private TimePeriod $weekdayHours;
    private TimePeriod $eveningHours;
    private TimePeriod $weekendHours;

    // constructor omitted - it just sets the above values

    public function __invoke(DateTimeImmutable $parkedAt, Vehicle $vehicle, Age $age): ParkingCharge
    {
        $total = new ParkingCharge(0, $this->currency);

        foreach (TimePeriod::hoursBetween($parkedAt, $this->clock->now()) as $hour) {
            $total = $total
                ->add($this->hourlySurcharge($hour))
                ->add($this->vehicleSurcharge($vehicle))
                ->subtract($this->ageDiscount($age));
        }

        return $total;
    }

    private function hourlySurcharge(Hour $hour): ParkingCharge
    {
        if ($this->weekdayHours->includes($hour)) {
            return $this->basicCharge;
        } elseif ($this->eveningHours->includes($hour)) {
            return $this->basicCharge->reduceBy(new Percent(20));
        } elseif ($this->weekendHours->includes($hour)) {
            return $this->basicCharge->reduceBy(new Percent(25));
        } else {
            return $this->basicCharge->add(new ParkingCharge(300, $this->currency));
        }
    }

    private function vehicleSurcharge(Vehicle $vehicle): ParkingCharge
    {
        if ($vehicle->is(Vehicle::Diesel())) {
            return new ParkingCharge(40, $this->currency);
        } elseif ($vehicle->is(Vehicle::Petrol())) {
            return new ParkingCharge(20, $this->currency);
        } else {
            return new ParkingCharge(0, $this->currency);
        }
    }

    private function ageDiscount(Age $age): ParkingCharge
    {
        if ($age->isOlderThan(new Age(59))) {
            return new ParkingCharge(100, $this->currency);
        } else {
            return new ParkingCharge(0, $this->currency);
        }
    }
}

Notice how it reads almost like the list rules above?

You see no raw/built in data structures like int or float or string for the type hints: only the types that are relevant to the domain itself. When implementing the value objects themselves, they would obviously "bottom-out" to those types (e.g. Time would carry just an int $hour and an int $minute), but it is a detail that is abstracted from the main logic of the calculator and pushed into the smaller objects (e.g. (Time‘s constructor would validate that 0 <= $hour < 24 and 0 <= $minute < 60).

The calculator can then be instantiated with the desired TimePeriods:

$weekdayHours = TimePeriod::betweenDaysWithTimes(
    DayRange::between(Day::Monday(), Day::Friday()),
    TimeRange::between(new Time(9, 0), new Time(18, 30)),
);

$eveningHours = TimePeriod::betweenDaysAndHours(
    DayRange::between(Day::Monday(), Day::Friday()),
    TimeRange::between(new Time(18, 30), new Time(22, 0)),
);

$weekendHours = TimePeriod::betweenDaysAndHours(
    DayRange::between(Day::Saturday(), Day::Sunday()),
    TimeRange::between(new Time(9, 0), new Time(22, 0)),
);

$calculate = new CalculateParkingCharge(
    new LocalClock(),
    new ParkingCharge(140, Currency::GBP()), // <- £1.40 per hour basic charge
    Currency::GBP(),
    $weekdayHours,
    $eveningHours,
    $weekendHours
);

$charge = $calculate(
    new DateTimeImmutable('2020-07-06 12:34:00'),
    Vehicle::Petrol(),
    new Age(61)
);

echo sprintf("You will pay %s", $charge->toString());

Now, you could completely forgo the domain model and just implement it just using the built in PHP data types in a transaction script. But consider how much logic you would be fitting into one place when doing so:

  • Getting the available hours correct (and configurable)
  • Doing money calculations correctly/validly (remember: we’re including percentage charges being applied, and it is easy to wrong with floats)
  • Applying the vehicle and age-based charges correctly

Instead, we’ve made the underlying concepts easily expressable, and if the business comes to us to ask for a change, it will be much easier to see where such a change would need to go, and to explain to them the feasibility and/or timescale of it.

Takeaways

So the moral of the story is not that you should use value objects everywhere for absolutely everything. But the value that they provide is in clear knowledge representation for the developer which is shared with the relevant business stakeholders.

Used strategically, it can make for a codebase that is far easier to understand; easier to onboard new developers into and easier to change when new requirements arise.