Creating an immutable value object in C# - Part II - Making the class better
Luca -
☕ 2 min. read
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.
0 Webmentions
These are webmentions via the IndieWeb and webmention.io.
16 Comments
Comments
Luca Bolognese's WebLog : Crea
2007-12-06T12:37:22ZPingBack from http://blogs.msdn.com/lucabol/archive/2007/12/03/creating-an-immutable-value-object-in-c-part-i-using-a-class.aspx
Tom Kirby-Green
2007-12-07T07:30:56ZThis 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!
Marcelo Cantos
2007-12-21T02:46:29ZFirst 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.
Marcelo Cantos
2007-12-21T02:48:05ZOops! Wherever I said 'new DateTime', I meant 'new DateSpan'.
lucabol
2007-12-21T12:36:35ZThanks 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 ...
Luca Bolognese's WebLog
2007-12-24T17:39:25ZOther posts: Part I - Using a class Part II - Making the class better In Part II I talked about the asymmetry
Noticias externas
2007-12-24T18:01:25ZOther posts: Part I - Using a class Part II - Making the class better In Part II I talked about the asymmetry
Luca Bolognese's WebLog
2007-12-28T18:45:33ZOther posts: Part I - Using a class Part II - Making the class better Part III - Using a struct In the
Noticias externas
2007-12-28T19:06:20ZOther posts: Part I - Using a class Part II - Making the class better Part III - Using a struct In the
MSDNArchive
2007-12-29T00:54:21ZLuca: Consider...
public DateTime Start { get; private set; }
lucabol
2007-12-29T18:15:50ZHey Kit,
Automatic properties don't prevent setting the property from inside the class. The readonly keyword does.
Luca Bolognese's WebLog
2008-01-11T13:36:12ZOther posts: Part I - Using a class Part II - Making the class better Part III - Using a struct Part
Noticias externas
2008-01-11T13:52:05ZOther posts: Part I - Using a class Part II - Making the class better Part III - Using a struct Part
Tales from the Evil Empire
2008-01-16T18:36:53ZFor some reason, there's been a lot of buzz lately around immutability in C#. If you're interested in
adamjcooper.com/blog
2008-06-03T15:37:59ZThe Quest for Quick-and-Easy Class-Based Immutable Value Objects in C# - Part 1: Introduction
adamjcooper.com/blog
2008-06-03T16:57:34ZThe Quest for Quick-and-Easy Immutable Value Objects in C#