Click here to Skip to main content
15,063,944 members
Articles / Programming Languages / C#
Technical Blog
Posted 18 Feb 2015

Tagged as

Stats

3.8K views
3 bookmarked

Make Those Value Objects Work For You

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
18 Feb 2015CPOL2 min read
How to make those value objects work for you.

We can have value objects be more than just information carriers. We can have them do some work for us.

A Scenario

Given a contract and some time sheet data for an employee, we need to calculate invoice lines for billing a client.

We can use an object called an InvoiceLineCalculator to calculate the resulting invoice lines. Even though an object like this feels different from the ValueObjects we talked about previously, I am going to argue that it can in fact be a ValueObject if we follow these rules:

  • It can be identified by the value of its outputs
  • It is immutable

The calculated invoice lines should always be the same given the same calculator input data, thus satisfying the first rule.

We satisfy immutability by enforcing two things:

  • After calculation, we should not be able to change the output values of the calculator. We achieve this by using private setters and concepts like IReadonlyList<t>.
  • We should also not keep references to input data. Even though we might pass complex objects to the calculator via its constructor, we should not keep references to these objects. Firstly, we might not want to keep these objects around as long as our calculator. Secondly, if we keep references around, we run the risk of depending on these references in our output, thus if another object changes values on our reference object, it might cause changes to our ValueObject output, breaking immutability.

But Can't We Just Use a Domain Service?

Short answer, yes! Long answer, it depends. For the simple example above, it might make sense to just have a domain service with a calculate method that returns the invoice lines. But let's say there are multiple contract types, each with its own invoicing rules. Furthermore, we might expect to be provided with other data regarding the calculation. This would mean our service has to know the calculations for all the different contract types. The service will soon become cluttered with details of each calculation.

Below is an example of an InvoiceLineCalculator as a ValueObject.

C#
public class WeeklyRetainerLineCalculator : IInvoiceLineCalculator  
{
    public bool ShouldInvoice { get; private set; }
    public IReadOnlyList<InvoiceLine> InvoiceLines { get; private set; }

    public WeeklyRetainerLineCalculator(Retainer retainer, List<Timesheet> timesheets )
    {
        Calculate(retainer, timesheets);
    }

    private void Calculate(Retainer retainer, List<Timesheet> timesheets)
    {
        //calculate invoice lines
    }
}

Having a single object per contract type proves to be cleaner. Note the immutability. Note the single responsibility. We will typically use a factory to construct our different calculator types based on input values. This means that when we need to add another contract/calculator type, we do not have to edit any control flow. We just add the decision line in our factory and implement the new calculator. Polymorph much?

Another great side effect of the above code is that it is easily testable and if needed the class can be removed with very little side effects. It is of course very important to name these classes well, to make the intent clear.

But Why

The most important thing to take away from this post is that immutability is your friend. When you set out to write immutable objects, intent stays clear, side-effects are limited and ultimately bugs are avoided.

This article was originally posted at http://feeds.feedburner.com/sneakycode

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

SneakyPeet
Software Developer (Senior)
Unknown
I write about Domain Driven Design, Clean Code and .net

Comments and Discussions

 
-- There are no messages in this forum --