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

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

Luca -

☕ 3 min. read

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 ob­ject with a C# class in­volves over­rid­ing Equals, Hashcode, == and !=. It is tricky, but usu­ally boil­er­plate stuff, well de­scribed in Wagner (2004). I don’t have Bill’s book in my of­fice, so I kind of made it up on the fly. It could be very wrong (the ==’ one looks very sus­pi­cious). Don’t copy it, read Bill’s book in­stead
    • 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 ex­ten­sion method IsSuperSet. This is some­thing I picked up from an old Coplien book, prob­a­bly this one. The con­cept is to keep meth­ods that don’t use in­ter­nal state of an ob­ject ex­ter­nal to the ob­ject it­self for the sake of fu­ture main­tain­abil­ity. The syn­tax for do­ing that was awk­ward be­fore, but ex­ten­sion meth­ods make it eas­ier
This works (it passes all the tests), but it has a num­ber of prob­lems. We’ll talk about these in Part II.

TimeLineAsClass - 1.zip

18 Comments

Comments

I am actually a little bit confused but I really belive that classes are reference objects in .NET; so naming "Creating immutable >>value objects<<...." looks a bit freak. Does not it?
So in this case the only thing a developer must do is to properly implement base functionality, i mean base functions available in object class and just protect all the internal data the real object contains.... So the question is what do you really want to write about, new stuff about implementation of base object's functionality that is quite well covered by Rihter and other authors or something new. sorry if i am too rude :) i just really cant get the clue of the article

Luca Bolognese's WebLog

2007-12-06T12:34:12Z

Other posts: Part I - Using a class In the previous post I showed how to trivially implement a value

Noticias externas

2007-12-06T12:51:18Z

Other posts: Part I - Using a class In the previous post I showed how to trivially implement a value

Kirill Osenkov

2007-12-07T15:21:29Z

One could probably replace
           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.Intersect(other) == other;
Just my 2 cents ;-)

Yep, one certainly could :)
I have always tended to state the obvious 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 getting old ...

Checklist for locking down an object:
1. Object must be constructed with constructor arguments that initialize all fields. Default constructor is hidden (private).  A class factory with a hidden constructor works as well.
2. Assignment operator does a deep copy of the object.  Member variables cannot be referred too by more than one object
3. Equality tests both == and != must test member variables and not the object pointer/reference
4. (optional) You cannot get a modifiable reference to the object (C++ only) or a pointer to the object
5. Compound properties, consisting of 2 or more member variables, must be modified all at one time (i.e., may not be modified individually).
6. (Allied Concept) Any data stored by the application must be accessible using stored procedures.  Do not access the tables directly.
7. (Allied Concept) Allow multiple copies of the object but have them refer to the same static data (i.e. a single copy of the data usually encapsulated in a static object).
This is standard practice in C++ from 1990 and even earlier than that in ADA and Smalltalk.
It is a good practice, but should be used with restraint because the modularity and/or productivity gains are small for each object using this pattern.  Taking modularity higher, such as the business function or major functional module level, usually is more effective.
Generally, over-emphasis on micro-modularity leads to longer development times and systems significantly harder to maintain.

Immutable values are created as structs in c#, if you set a struct variable to another variable of the same type as in:
struct1 var1 = new struct1(field1, field2);
struct1 var2 = var1;
var2 ends up being a deep copy of var1 stored on a seperate memory location. That's what an immutable type is, if you do that with your class, var2 will be a pointer to the same memory location where var1's value is stored, therefore it is NOT an immutable type.
Incidentally, except for #4, #5 and #6 on Greg's checklist all other checklist items are true characteristics of a .NET structure. 1) Default constructor is hidden and ctor initializes all fields, 2) assignment operator does a deep copy of the object (as I mentioned above), 3) equality tests test the value, not the pointer, and 7) it allows multiple copies of the object but all of them refer 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 better In Part II I talked about the asymmetry

Noticias externas

2007-12-24T18:01:24Z

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:18Z

Other posts: Part I - Using a class Part II - Making the class better 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 better 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 better Part III - Using a struct Part

Tales from the Evil Empire

2008-01-16T18:36:52Z

For some reason, there's been a lot of buzz lately around immutability in C#. If you're interested 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 talking

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#

0 Webmentions

These are webmentions via the IndieWeb and webmention.io.