Creating an immutable value object in C# - Part I - Using a class

-

Other posts:

Value ob­jects are ob­jects for which the iden­tity is based on their state in­stead of their pointer in mem­ory. For ex­am­ple, a nu­meric Complex class is, most of the time, a value ob­ject be­cause you can treat two in­stances as the same if their state (real and img fields in this case) is the same. An im­mutable value ob­ject is a value ob­ject that can­not be changed. You can­not mod­ify its state, you have to cre­ate new ones.

I’m us­ing these guys more and more in my code for a num­ber of rea­sons, both prac­ti­cal and philo­soph­i­cal. The prac­ti­cal ones re­volve around the greater ro­bust­ness of pro­gram­ming with­out side ef­fects and the greater sim­plic­ity of par­al­leliz­ing your code. The philo­soph­i­cal ones are more in­ter­est­ing (and sub­jec­tive). When in my de­sign process I spend the time to ag­gres­sively look­ing for these kinds of ob­jects, the re­sult­ing de­sign ends up cleaner. I es­pe­cially like when I can de­fine some sort of close al­ge­bra for these guys (i.e. a set of func­tions that op­er­ate over them and pro­duces new ones, not un­like +’ and -’ for num­bers).

This se­ries de­scribes how to cre­ate im­mutable value ob­jects in C# and the de­sign de­ci­sions in­volved. This is a sum­mary of an email thread I had with Mads and Luke.

The con­cept I will use for this se­ries is a DateSpan. As I de­fine it, a DateSpan has a Start and an End date. You can ask for the DataS­pan that rep­re­sents the Union and Intersection of two DateSpans. The tests in the at­tached code bet­ter de­fine the be­hav­ior of these op­er­a­tions.

Given that I never use structs in my code (I’m a min­i­mal­ist lan­guage user), I’ll start by us­ing a class to rep­re­sent it. We’ll make this class bet­ter in part II and use a struct in part III. A first stab at it is as fol­lows:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
public class DateSpan {
    public DateSpan(DateTime start, DateTime end) {
        if (end < start)
            throw new ArgumentException(start.ToString() + " doesn't come before " + end.ToString());
        Start = start;
        End = end;
    }
    public DateSpan Union(DateSpan other) {
        if (other == null)
            return new DateSpan(Start, End);
        if (IsOutside(other))
            return null;
        DateTime start = other.Start < Start ? other.Start : Start;
        DateTime end = other.End > End ? other.End : End;
        return new DateSpan(start, end);
    }
    public DateSpan Intersect(DateSpan other) {
        if (other == null)
            return null;
        if (IsOutside(other))
            return null;
        DateTime start = other.Start > Start ? other.Start : Start;
        DateTime end = other.End < End ? other.End : End;
        return new DateSpan(start, end);
    }
    private bool IsOutside(DateSpan other) {
        return other.Start > End || other.End < Start;
    }
    public DateTime Start { get; private set; }
    public DateTime End { get; private set; }
    #region Boilerplate Equals, ToString Implementation
    public override string ToString() {
        return string.Format("Start:{0} End:{1}", Start, End);
    }
    public override bool Equals(object obj) {
        if (obj == null) return false;
        if (this.GetType() != obj.GetType()) return false;
        DateSpan other = obj as DateSpan;
        return other.End == End && other.Start == Start;
    }
    public override int GetHashCode() {
        return Start.GetHashCode() | End.GetHashCode();
    }
    public static Boolean operator ==(DateSpan v1, DateSpan v2) {
        if ((object)v1 == null)
            if ((object)v2 == null)
                return true;
            else
                return false;
        return (v1.Equals(v2));
    }
    public static Boolean operator !=(DateSpan v1, DateSpan v2) {
        return !(v1 == v2);
    }
    #endregion
}
public static class TimeLineExtensions {
    public static bool IsSuperSet(this DateSpan span, DateSpan other) {
        if (span.Intersect(other) == other)
            return true;
        return false;
    }
}

Some things to no­tice in this code:

  1. Defining a value object with a C# class involves overriding Equals, Hashcode, == and !=. It is tricky, but usually boilerplate stuff, well described in Wagner (2004). I don’t have Bill’s book in my office, so I kind of made it up on the fly. It could be very wrong (the ‘==’ one looks very suspicious). Don’t copy it, read Bill’s book instead
    • Defining an im­mutable ob­ject with a C# class in­volves dis­ci­pline in not chang­ing the pri­vate state (we’ll see in Part II that we can do bet­ter than discipline’)

      • Notice the extension method IsSuperSet. This is something I picked up from an old Coplien book, probably this one. The concept is to keep methods that don’t use internal state of an object external to the object itself for the sake of future maintainability. The syntax for doing that was awkward before, but extension methods make it easier
This works (it passes all the tests), but it has a number of problems. We’ll talk about these in Part II.

TimeLineAsClass - 1.zip

Tags

18 Comments

Comments

I am ac­tu­ally a lit­tle bit con­fused but I re­ally be­live that classes are ref­er­ence ob­jects in .NET; so nam­ing Creating im­mutable >>value ob­jects<<….” looks a bit freak. Does not it?
So in this case the only thing a de­vel­oper must do is to prop­erly im­ple­ment base func­tion­al­ity, i mean base func­tions avail­able in ob­ject class and just pro­tect all the in­ter­nal data the real ob­ject con­tains…. So the ques­tion is what do you re­ally want to write about, new stuff about im­ple­men­ta­tion of base ob­jec­t’s func­tion­al­ity that is quite well cov­ered by Rihter and other au­thors or some­thing new. sorry if i am too rude :) i just re­ally cant get the clue of the ar­ti­cle

Luca Bolognese's WebLog

2007-12-06T12:34:12Z

Other posts: Part I - Using a class In the pre­vi­ous post I showed how to triv­ially im­ple­ment a value

Noticias externas

2007-12-06T12:51:18Z

Other posts: Part I - Using a class In the pre­vi­ous post I showed how to triv­ially im­ple­ment a value

Kirill Osenkov

2007-12-07T15:21:29Z

One could prob­a­bly re­place
           if ((object)v2 == null)
               return true;
           else
               return false;
with
           return (object)v2 == null;
and
       if (span.Intersect(other) == other)
           return true;
       return false;
with
       return span.In­ter­sect(other) == other;
Just my 2 cents ;-)

Yep, one cer­tainly could :)
I have al­ways tended to state the ob­vi­ous in my code. Once upon a time I thought it to be a bad thing. Now I think it is good.
Maybe I’m just get­ting old …

Checklist for lock­ing down an ob­ject:
1. Object must be con­structed with con­struc­tor ar­gu­ments that ini­tial­ize all fields. Default con­struc­tor is hid­den (private).  A class fac­tory with a hid­den con­struc­tor works as well.
2. Assignment op­er­a­tor does a deep copy of the ob­ject.  Member vari­ables can­not be re­ferred too by more than one ob­ject
3. Equality tests both == and != must test mem­ber vari­ables and not the ob­ject pointer/​ref­er­ence
4. (optional) You can­not get a mod­i­fi­able ref­er­ence to the ob­ject (C++ only) or a pointer to the ob­ject
5. Compound prop­er­ties, con­sist­ing of 2 or more mem­ber vari­ables, must be mod­i­fied all at one time (i.e., may not be mod­i­fied in­di­vid­u­ally).
6. (Allied Concept) Any data stored by the ap­pli­ca­tion must be ac­ces­si­ble us­ing stored pro­ce­dures.  Do not ac­cess the ta­bles di­rectly.
7. (Allied Concept) Allow mul­ti­ple copies of the ob­ject but have them re­fer to the same sta­tic data (i.e. a sin­gle copy of the data usu­ally en­cap­su­lated in a sta­tic ob­ject).
This is stan­dard prac­tice in C++ from 1990 and even ear­lier than that in ADA and Smalltalk.
It is a good prac­tice, but should be used with re­straint be­cause the mod­u­lar­ity and/​or pro­duc­tiv­ity gains are small for each ob­ject us­ing this pat­tern.  Taking mod­u­lar­ity higher, such as the busi­ness func­tion or ma­jor func­tional mod­ule level, usu­ally is more ef­fec­tive.
Generally, over-em­pha­sis on mi­cro-mod­u­lar­ity leads to longer de­vel­op­ment times and sys­tems sig­nif­i­cantly harder to main­tain.

Immutable val­ues are cre­ated as structs in c#, if you set a struct vari­able to an­other vari­able of the same type as in:
struct1 var1 = new struct1(field1, field2);
struct1 var2 = var1;
var2 ends up be­ing a deep copy of var1 stored on a seper­ate mem­ory lo­ca­tion. That’s what an im­mutable type is, if you do that with your class, var2 will be a pointer to the same mem­ory lo­ca­tion where var1′s value is stored, there­fore it is NOT an im­mutable type.
Incidentally, ex­cept for #4, #5 and #6 on Greg’s check­list all other check­list items are true char­ac­ter­is­tics of a .NET struc­ture. 1) Default con­struc­tor is hid­den and ctor ini­tial­izes all fields, 2) as­sign­ment op­er­a­tor does a deep copy of the ob­ject (as I men­tioned above), 3) equal­ity tests test the value, not the pointer, and 7) it al­lows mul­ti­ple copies of the ob­ject but all of them re­fer to the same data.

Luca Bolognese's WebLog

2007-12-24T17:39:24Z

Other posts: Part I - Using a class Part II - Making the class bet­ter In Part II I talked about the asym­me­try

Noticias externas

2007-12-24T18:01:24Z

Other posts: Part I - Using a class Part II - Making the class bet­ter In Part II I talked about the asym­me­try

Luca Bolognese's WebLog

2007-12-28T18:45:33Z

Other posts: Part I - Using a class Part II - Making the class bet­ter Part III - Using a struct In the

Noticias externas

2007-12-28T19:06:18Z

Other posts: Part I - Using a class Part II - Making the class bet­ter Part III - Using a struct In the

Luca Bolognese's WebLog

2008-01-11T13:36:11Z

Other posts: Part I - Using a class Part II - Making the class bet­ter Part III - Using a struct Part

Noticias externas

2008-01-11T13:52:04Z

Other posts: Part I - Using a class Part II - Making the class bet­ter Part III - Using a struct Part

Tales from the Evil Empire

2008-01-16T18:36:52Z

For some rea­son, there’s been a lot of buzz lately around im­mutabil­ity in C#. If you’re in­ter­ested in

brute forced brilliance

2008-04-03T13:12:58Z

It’s Time for a Change — We need Immutable Types

Luca Bolognese's WebLog

2008-04-21T13:35:43Z

Previous posts: Part I - Background Part II - Tuples Now that we know what Tuples are, we can start talk­ing

adamjcooper.com/blog

2008-06-03T15:37:37Z

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

adamjcooper.com/blog

2008-06-03T16:57:17Z

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