Representing a number with no more than two decimal places

How encapsulation makes it almost impossible to use a value that breaks a business rule

I was recently reviewing some code and saw the developer had a property on a class which held a number. The business rule was that the number should always be rounded to 2 decimal places, but the developer was unable to encode this logic in any way. Instead, every time they wanted to update the value, they had to remember to round it first.

This got me thinking about how we could design a class which encapsulates the business rule that the number should always be rounded, so that a developer setting the value of this number doesn't need to remember to round it.

This is an example of trying to avoid the code smell known as Primitive Obsession. This code smell is representing as much data as possible as a primitive (in this case, a number) rather than a more specialised class. The downside is that all business rules relating to a piece of data have to be executed separately, rather than forming part of the definition of the data. This means it's easy to forget about them and inadvertently create invalid data (in this case, assigning a value with more than 2 decimal places should be considered invalid).

This is what I came up with. I've written a NumberWithUpToTwoDecimalPlaces class which encapsulates the business rule, and a Tester class which has a property to which the business rule should be applied. I've then updated the property on that class several times and logged the results to the console. (This is written in typescript but the javascript equivalent is almost identical.)

class NumberWithUpToTwoDecimalPlaces {
    private value: number;

    constructor(value: number) {
        this.value = this.reduceToTwoDecimalPlaces(value);
    }

    setValue(value: number) {
        this.value = this.reduceToTwoDecimalPlaces(value);
    }

    getValue(): number {
        return this.value;
    }

    /** The implementation of this function is arbitrary.
     *  The point is that it gets run whenever we set a value,
     *  and the developer updating a property value doesn't 
     *  have to remember to manually call this function. */
    private reduceToTwoDecimalPlaces(value: number) {
        return parseFloat(value.toFixed(2));
    }
}

class Tester {
    private _num = new NumberWithUpToTwoDecimalPlaces(0);
    get num() {
        return this._num.getValue();
    }
    set num(value: number) {
        this._num.setValue(value);
    }
}

const tester = new Tester();
console.log(tester.num); // 0

tester.num = 5;
console.log(tester.num); // 5

tester.num = 5.98451654;
console.log(tester.num); // 5.98

tester.num = 6.4323;
console.log(tester.num); // 6.43

You can view this interactively in the typescript playground if you would like to play around with it.

The point of this is that for a developer working with the Tester class and setting the property of num, it's almost impossible to go wrong. You don't need to remember to round anything, you can just put in whatever number you like and the NumberWithUpToTwoDecimalPlaces class will deal with it for you.

The cost of this is some extra code which needs to be maintained. There is a one-off cost, which is writing the NumberWithUpToTwoDecimalPlaces class in the first place. There is then an ongoing cost, which is the boilerplate for having a getter and setter every time you use the class. (This isn't strictly necessary, but makes things easier for other developers and makes it harder to make mistakes.)

The benefit which you gain from this cost is that it minimises the risk of a mistake to near zero when it comes to this business rule. It's far more work to get it wrong than it is to get it right.

How would you have implemented this requirement? What are the costs & benefits of an alternative solution?