Creating an immutable value object in C# - Part II - Making the class better - Luca Bolognese

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

Luca -

☕ 2 min. read

Other posts:

In the pre­vi­ous post I showed how to triv­ially im­ple­ment a value ob­ject. The code works but it has sev­eral is­sues. Some are very sim­ple, oth­ers are more in­ter­est­ing.

Let’s take a look at them:

  • State not ex­plic­itly read-only
  • Asymmetry in the us­age of Union and Intersection
  • Small perf is­sue in the Union method

The first prob­lem is that my use of au­to­matic prop­er­ties does­n’t as­sure that the sta­tus of the ob­ject is im­mutable; I can still mod­ify it from in­side the class. The sim­ple so­lu­tion is to make the fields read­only and write the get­ters as in:

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

The sec­ond is­sue is more sub­tle. 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 lit­tle algebra’ to be sym­met­ric. In this case I’d like: r1 == r2 == null. But this code throws in the last line as I’m try­ing to in­voke a method on a null ob­ject.

The tra­di­tional so­lu­tion to this prob­lem is to make Union and Intersect to be sta­tic meth­ods, but then you loose the magic of call­ing them as in­stance meth­ods (i.e. it be­comes hard to chain them to­gether as in d1.In­ter­sect(d2).Union(d3).In­ter­sect(d4)).

Extension meth­ods come to the res­cue here as they al­low you to cre­ate sta­tic meth­ods, but to call them as if the were in­stance meth­ods. The new code for Intersect looks like the fol­low­ing:

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 ex­ten­sion method needs to ac­cess pri­vate state of the class. In that case you would need to cre­ate a sta­tic method on the DataSpan class and in­voke it from the ex­ten­sion method. Slightly more con­vo­luted, but still doable.

At a more philo­soph­i­cal level, the asym­me­try is­sue hap­pens here be­cause I’m us­ing some­thing out­side my do­main of in­ter­est (the null value) to rep­re­sent a spe­cial value in­side my do­main of in­ter­est. More on this as we talk about structs in up­com­ing posts.

The last point is a very small one. In the Union method I am cre­at­ing a new ob­ject un­nec­es­sar­ily in the fol­low­ing line:

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

I can ob­vi­ously avoid it by just re­turn­ing this.

This post hints to sev­eral pos­si­ble is­sues. Is it a good idea to use null to rep­re­sent spe­cial val­ues in my do­main? What if I have more than one of them (i.e. pos­i­tive/​neg­a­tive in­fi­nite)? Would us­ing structs solve these prob­lems?

We’ll take a look at these op­tions in up­com­ing posts. Attached is the mod­i­fied code.

16 Comments

Comments

Tom Kirby-Green

2007-12-07T07:30:56Z

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!

Marcelo Cantos

2007-12-21T02:46:29Z

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.

Marcelo Cantos

2007-12-21T02:48:05Z

Oops! Wherever I said 'new DateTime', I meant 'new DateSpan'.

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 ...

Luca Bolognese's WebLog

2007-12-24T17:39:25Z

Other 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:25Z

Other 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:33Z

Other 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:20Z

Other posts: Part I - Using a class Part II - Making the class better Part III - Using a struct In the

MSDNArchive

2007-12-29T00:54:21Z

Luca: Consider...
public DateTime Start { get; private set; }

Hey 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:12Z

Other posts: Part I - Using a class Part II - Making the class better Part III - Using a struct Part

Noticias externas

2008-01-11T13:52:05Z

Other 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:53Z

For 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:59Z

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

adamjcooper.com/blog

2008-06-03T16:57:34Z

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

0 Webmentions

These are webmentions via the IndieWeb and webmention.io.