Creating an immutable value object in C# – Part II – Making the class better


Other posts:



In the previous post I showed how to trivially implement a value object. The code works but it has several issues. Some are very simple, others are more interesting.


Let’s take a look at them:



  • State not explicitly read-only

  • Asymmetry in the usage of Union and Intersection

  • Small perf issue in the Union method

The first problem is that my use of automatic properties doesn’t assure that the status of the object is immutable; I can still modify it from inside the class. The simple solution is to make the fields readonly and write the getters as in:

    private readonly DateTime start;
private readonly DateTime end;

public DateTime Start { get { return start; } }
public DateTime End { get { return end; } }


The second issue is more subtle. Consider this code

        DateSpan d1 = new DateSpan(new DateTime(1, 1, 2000), new DateTime(1, 1, 2002));
DateSpan d2 = null;

DateSpan r1 = d1.Intersect(d2);
DateSpan r2 = d2.Intersect(d1);


I would like things in my little ‘algebra’ to be symmetric. In this case I’d like: r1 == r2 == null. But this code throws in the last line as I’m trying to invoke a method on a null object.


The traditional solution to this problem is to make Union and Intersect to be static methods, but then you loose the magic of calling them as instance methods (i.e. it becomes hard to chain them together as in d1.Intersect(d2).Union(d3).Intersect(d4)).


Extension methods come to the rescue here as they allow you to create static methods, but to call them as if the were instance methods. The new code for Intersect looks like the following:

    public static DateSpan Intersect(this DateSpan one, DateSpan other) {

if (one == null)
return null;

if (other == null)
return null;

if (one.IsOutside(other))
return null;

DateTime start = other.Start > one.Start ? other.Start : one.Start;
DateTime end = other.End < one.End ? other.End : one.End;

return new DateSpan(start, end);
}


This workaround would not work if the extension method needs to access private state of the class. In that case you would need to create a static method on the DataSpan class and invoke it from the extension method. Slightly more convoluted, but still doable.


At a more philosophical level, the asymmetry issue happens here because I’m using something outside my domain of interest (the null value) to represent a special value inside my domain of interest. More on this as we talk about structs in upcoming posts.


The last point is a very small one. In the Union method I am creating a new object unnecessarily in the following line:

        if (other == null)
return new DateSpan(Start, End);

I can obviously avoid it by just returning “this“.


This post hints to several possible issues. Is it a good idea to use null to represent special values in my domain? What if I have more than one of them (i.e. positive/negative infinite)? Would using structs solve these problems?


We’ll take a look at these options in upcoming posts. Attached is the modified code.

Comments (16)

  1. Tom Kirby-Green says:

    This is shaping up to be a very timely and useful mini series Luca 🙂 Please don’t keep us waiting too long for the next part!

  2. Marcelo Cantos says:

    First off, the most practical representation of date spans is an inclusive lower bound and exclusive upper bound, i.e., [start, end). Equally important, they should be treated as points in time (which is what DateTime represents), not complete days. Thus, new DateTime(d, d) is empty for any d (solving empty ranges) and new DateTime(d, d.AddDays(1)) is exactly one day.

    Also, the type should really be DateTimeSpan.

    Convenience properties such as a static DateSpan.Empty would come in handy.

    Finally, DateTime has MaxValue and MinValue, which serve as fairly natural surrogates for +/- infinity, and also eliminate edge-cases from set operations.

  3. Marcelo Cantos says:

    Oops! Wherever I said ‘new DateTime’, I meant ‘new DateSpan’.

  4. lucabol says:

    Thanks for the comment. It makes me think of something an old functional guy said once: "The idea of reusing objects across domain boundaries is absurd, not even something as simple as Person can be defined the same way in different domains".

    In my domain (a stock backtesting app) a DateSpan needs to have a day boundary, not a point in time boundary. Also, inclusive lower and upper bounds have been working pretty well for my app so far (even if I can see that your definition has conceptual appeal).

    And anyhow, I’m just trying to show how to use some language features. I don’t care much about the particular sample. I could have chosen Complex, but I thought it was too boring …

  5. Other posts: Part I – Using a class Part II – Making the class better In Part II I talked about the asymmetry

  6. Other posts: Part I – Using a class Part II – Making the class better In Part II I talked about the asymmetry

  7. Other posts: Part I – Using a class Part II – Making the class better Part III – Using a struct In the

  8. Other posts: Part I – Using a class Part II – Making the class better Part III – Using a struct In the

  9. MSDN Archive says:

    Luca: Consider…

    public DateTime Start { get; private set; }

  10. lucabol says:

    Hey Kit,

    Automatic properties don’t prevent setting the property from inside the class. The readonly keyword does.

  11. Other posts: Part I – Using a class Part II – Making the class better Part III – Using a struct Part

  12. Other posts: Part I – Using a class Part II – Making the class better Part III – Using a struct Part

  13. For some reason, there’s been a lot of buzz lately around immutability in C#. If you’re interested in

  14. The Quest for Quick-and-Easy Class-Based Immutable Value Objects in C# – Part 1: Introduction

  15. The Quest for Quick-and-Easy Immutable Value Objects in C#