Creating an immutable value object in C# - Part I - Using a class
Luca -
☕ 3 min. read
Other posts:
- Part II - Making the class better{.}
- Part III - Using a struct{.}
- Part IV - A class with a special value{.}
Value objects are objects for which the identity is based on their state instead of their pointer in memory. For example, a numeric Complex class is, most of the time, a value object because you can treat two instances as the same if their state (real and img fields in this case) is the same. An immutable value object is a value object that cannot be changed. You cannot modify its state, you have to create new ones.
I’m using these guys more and more in my code for a number of reasons, both practical and philosophical. The practical ones revolve around the greater robustness of programming without side effects and the greater simplicity of parallelizing your code. The philosophical ones are more interesting (and subjective). When in my design process I spend the time to aggressively looking for these kinds of objects, the resulting design ends up cleaner. I especially like when I can define some sort of close algebra for these guys (i.e. a set of functions that operate over them and produces new ones, not unlike ‘+’ and ‘-’ for numbers).
This series describes how to create immutable value objects in C# and the design decisions involved. This is a summary of an email thread I had with Mads and Luke.
The concept I will use for this series is a DateSpan. As I define it, a DateSpan has a Start and an End date. You can ask for the DataSpan that represents the Union and Intersection of two DateSpans. The tests in the attached code better define the behavior of these operations.
Given that I never use structs in my code (I’m a minimalist language user), I’ll start by using a class to represent it. We’ll make this class better in part II and use a struct in part III. A first stab at it is as follows:
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 notice in this code:
- 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 immutable object with a C# class involves discipline in not changing the private state (we’ll see in Part II that we can do better 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
-
0 Webmentions
These are webmentions via the IndieWeb and webmention.io.
18 Comments
Comments
AlexVB
2007-12-06T01:13:48ZI 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:12ZOther posts: Part I - Using a class In the previous post I showed how to trivially implement a value
Noticias externas
2007-12-06T12:51:18ZOther posts: Part I - Using a class In the previous post I showed how to trivially implement a value
Kirill Osenkov
2007-12-07T15:21:29ZOne 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 ;-)
Luca
2007-12-17T13:07:35ZYep, 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 ...
Greg
2007-12-18T12:40:51ZChecklist 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.
fernando
2007-12-24T12:38:02ZImmutable 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:24ZOther 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:24ZOther 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:18ZOther 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:11ZOther posts: Part I - Using a class Part II - Making the class better Part III - Using a struct Part
Noticias externas
2008-01-11T13:52:04ZOther 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:52ZFor 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:58ZIt's Time for a Change -- We need Immutable Types
Luca Bolognese's WebLog
2008-04-21T13:35:43ZPrevious 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:37ZThe Quest for Quick-and-Easy Class-Based Immutable Value Objects in C# - Part 1: Introduction
adamjcooper.com/blog
2008-06-03T16:57:17ZThe Quest for Quick-and-Easy Immutable Value Objects in C#