The Joy of Read-Only Getters
Read-Only getters (read-only properties in C#) are an excellent option for managing volatile object state that should always be up to date, like a calculated value or formatted string.
In a previous write-up on whether we ought to favour object construction or object initialisation, we produced a constructor for an InvoiceLine
class. Today we’ll improve on our work.
Again, here is the constructor for InvoiceLine
:
public InvoiceLine(Product product, decimal quantity) { Product = product; Quantity = quantity; UnitPrice = product.UnitPrice; Subtotal = UnitPrice * Quantity; }
Say, we had the following unit test:
[Fact] public void When_Construct_InvoiceLine_Then_Calculates_Subtotal() { var apple = new Product("Apple", unitPrice: 2); var line = new InvoiceLine(apple, quantity: 3); line.Subtotal.Should().Be(6); // = 2 * 3 }
Would the test pass? I reckon it would. Subtotal
is the product of Quantity
(here 2), and UnitPrice
(here 3). 3 times 2 equals 6
What if we had another unit test? One where we change the Quantity
. Would that unit test pass? Here’s a representative test:
[Fact] public void When_Change_Quantity_Then_Recalculates_Subtotal() { var apple = new Product("Apple", unitPrice: 2); var line = new InvoiceLine(apple, quantity: 3); line.Quantity = 5; line.Subtotal.Should().Be(10); // = 2 * 5 }
Unfortunately, this new unit test will fail. The error message will be something like ‘Expected Subtotal to be 10, but was actually 6‘.
Hmm, the Subtotal
amount after construction was also 6. It looks like it hasn’t updated.
And fair enough too. We only assign the Subtotal
in the constructor. The value cannot change for an InvoiceLine
object after that time.
OK, but how do we fix this? How do we get this unit test to pass?
Here is one way:
public decimal Subtotal { get { return Quantity * UnitPrice; } };
and rewritten to be more modern and concise,
public decimal Subtotal => Quantity * UnitPrice;
Alright, we have changed Subtotal
into a calculated read-only property.
But now the Subtotal
assignment in the constructor will give us a compilation error. We must remove it:
public InvoiceLine(Product product, decimal quantity) { Product = product; Quantity = quantity; UnitPrice = product.UnitPrice; }
So much neater. In my opinion, the constructor was starting to look a bit bloated with four separate assignment lines.
The advantages of moving the Subtotal
into a separate read-only property are:
- It’s more up-to-date: The Subtotal recalculates just-in-time using the values in
Quantity
andUnitPrice
. In comparison, assigning theSubtotal
in the constructor will not alter the value whenQuantity
,Unit Price
or both change. - It’s Tidier: Having the
Subtotal
as its own property, rather than bloating the constructor with another assignment line, is so much neater. - It’s Safer – Using the lambda arrow notation, the
Subtotal
property is guaranteed to be read-only – we cannot assign a value to it. In the case of the constructor assignment, it’s too easy to inadvertently leave the Subtotal property declaration open with a public setter.
When needing to expose up-to-date calculated values, consider read-only getters.
Leave a Reply
Want to join the discussion?Feel free to contribute!