A logbook for curious waters
← Software Engineering Field Guide

Replace Data Value with Object

Before you begin: Classes, constructors, primitive types, and validation.

Replace Data Value with Object replaces a primitive such as a string or integer with a small object representing its meaning. The object can give related validation and operations one home. The modern catalog calls this Replace Primitive with Object. 1

Original version: every owner checks a quantity

A cart line permits a nonnegative integer quantity. Other parts of the application also use that same quantity rule.

public sealed class CartLine
{
    public int Quantity { get; }
    public CartLine(int quantity)
    {
        if (quantity < 0) throw new ArgumentOutOfRangeException(nameof(quantity));
        Quantity = quantity;
    }
}

The field has the same language-level type as a customer identifier or a temperature. Its specific rule lives in the cart constructor.

class CartLine:
    def __init__(self, quantity):
        if type(quantity) is not int or quantity < 0:
            raise ValueError("Quantity must be a nonnegative integer.")
        self.quantity = quantity

The constructor checks the rule, but another class accepting a quantity would have to repeat or share that validation somehow.

Refactored version: construct a quantity first

public sealed class Quantity
{
    public int Value { get; }
    public Quantity(int value)
    {
        if (value < 0) throw new ArgumentOutOfRangeException(nameof(value));
        Value = value;
    }
}

public sealed class CartLine
{
    public Quantity Quantity { get; }
    public CartLine(Quantity quantity)
    {
        Quantity = quantity ?? throw new ArgumentNullException(nameof(quantity));
    }
}

The caller writes new CartLine(new Quantity(3)). The quantity object establishes its own rule; the cart only needs a non-null quantity.

class Quantity:
    def __init__(self, value):
        if type(value) is not int or value < 0:
            raise ValueError("Quantity must be a nonnegative integer.")
        self._value = value

    @property
    def value(self):
        return self._value


class CartLine:
    def __init__(self, quantity):
        if not isinstance(quantity, Quantity):
            raise TypeError("Expected a Quantity.")
        self.quantity = quantity

CartLine(Quantity(3)) makes the intended concept visible. The public property does not permit assigning a new numeric value to the quantity, though Python’s private attribute remains accessible by convention.

Add meaning, not packaging

The valid quantity and its numeric result are preserved, but invalid input now fails while constructing Quantity. Public signatures and some exception details change, so callers and tests need migration.

This example does not implement value-based equality; two quantity objects are distinct objects even when their numbers match. Add equality deliberately when the domain requires it.

A loop counter does not need this wrapper. A quantity shared by several APIs may benefit when one named type prevents mistakes or centralizes rules. Avoid wrappers that merely force everyone to type .Value without providing any useful meaning or protection.

Sources

  1. Martin Fowler — Replace Primitive with Object, formerly Replace Data Value with Object.