In my book, I present an example of a
Range<T> class do demonstrate iterator blocks. The range allows you to iterate over each element within it in the obvious fashion. There’s an abstract base class, and then a couple of concrete classes derived from that – enough to show the pattern. The base class is abstract because there’s a single abstract method,
GetNextValue, which is required to take a current value and return the next one in the sequence. How this occurs depends on the types involved – in the case of a range of
DateTime elements, it will add a
TimeSpan each time, for instance, whereas an
Int32Range will just add an
int. Here’s a class diagram of how it looks:
The requirements for the code in the book were very simplistic, in order to be able to present all the code on the printed page. However, I wanted to expand this in MiscUtil and “do the job properly”. In particular, I wanted to be able to:
- Reverse a range
- Make a range exclusive (at the “far end” – a half-open interval)
- Make an exclusive range inclusive
- Do all of this while keeping the immutable nature of the code from the book
When trying to implement this, I discovered it was actually quite tricky. In particular, when using inheritance I ran into some obstacles:
- Unless we use the original range as a proxy, creating a new range based on the original is tricky. We basically need to clone, and that’s fraught in various ways.
MemberwiseClonewill work in many situations, but it’s inelegant – and we can’t keep the fields marked
readonlyand still modify the cloned copy.
- Reversing a range using just the original type constraint of
T : IComparable<T>is a bit of a pain. You need to keep remembering which way to compare things. This is a bit of an aside, but using an
IComparer<T>instead is a lot simpler – it’s really easy to build a new IComparer<T> which proxies to the original one and reverses the order of the parameters.
- There’s no guarantee that just because the base class has no mutable data, the derived class will do likewise.
In addition, I realised I was using inheritance in a way that went against what I’d written near the end of the book: when using inheritance in a very limited way, consider using delegates instead. A
Range<T> only needs extra behaviour to be specified in terms of comparisons (
IComparer<T>) and how to take a step from one value to the next. The latter can easily be represented as a
Func<T,T> in .NET 3.5.
My new design has a single sealed, immutable class:
There are still a few ways in which this isn’t ideal:
- You can specify a null step function, in which case you can’t iterate over the range. I’d prefer the type to not implement
IEnumerable<T>if it can’t do the job properly.
- You have to specify a reverse step function if you want to iterate over the reverse of the range.
- There are a heck of a lot of constructor overloads.
Now, none of these are horrendous, and I think it’s a lot nicer than it was before. I’ve currently got an additional non-generic
Range class with a bunch of overloaded methods for creating ranges of various types. I can’t think of a decent name for these methods at the moment, so currently you’d write:
- Range.Of(1, 5) // 1, 2, 3, 4, 5
- Range.Of(1, 5).Exclusive() // 1, 2, 3, 4
- Range.Of(1, 5, 2) // 1, 3, 5
- Range.Of(DateTime.Today, DateTime.Now, TimeSpan.FromMinutes(1)) // Midnight, 1 minute past etc
I think it might be nicer to use extension methods for these, to allow:
In order to do this nicely I may need to expose the comparer in
Range<T> as well, but I don’t think that’s really a problem. Thoughts on this are welcome.
Anyway, the broad point of this post (other than to hopefully relieve my own insomnia – and possibly yours too) is that immutability and inheritance don’t mix terribly nicely, especially when you want to effectively clone an instance and modify some aspects. That’s not terribly surprising, but it is interesting – and it fits in with the experience that inheritance doesn’t mix terribly nicely with equality comparisons, either.
8 thoughts on “Immutability and inheritance”
I like the readability of the extensions method approach. I’m definitely going to “borrow” it in some code I wrote the other day for my own Range class. :-)
I prefer to use immutable objects, but I hate writing them! I have not run into issues with inheritance as I’ve generally followed the approach that immutable objects are essentially value objects. With that in mind, they should be sealed and not participate in inheritance. The design issue that I have found most frustrating is with initialization of the objects and copying them. You can do it in C#, but it takes a bit more effort to come up with an aesthetically pleasing, easy to read syntax that works. Usually I end up doing one of two things:
* Passing in functions
* Using mutable builder classes
As an aside, it’s interesting to note that we took similar approaches to our implementations. I created a sealed, immutable Range class. To make it easier to work with, I also created a static Range class. Functionality-wise our designs do differ from there. Your class generates values where my class serves to test to see if a value is in the range. The approach of using a generic class and a static helper class seems to be a common pattern. With type inferencing it works well and blends in.
The thing that I am saying might not be completely irrelevant.
Python has a concept of list.
so a string is a list of characters. And,
a = “Jon”
print a[0,1,1] — parameters are start, end, step
To reverse the string, you would use
Dynamic languages has lot of powerful way of expressing range based things. Ruby comes to mind.
While dynamic languages often do have built-in list syntax (and often maps as well) I don’t think there’s anything particularly dynamic about it. It’s a coincidence more than anything else, I think.
It would be possible for C# to build ranges, lists and maps into the syntax – but unlikely, I suspect, due to the difficulties of avoiding breaking existing code.
Base abstract generic class is a bad choice in most situations.
You might also think of splitting this into more than one abstraction. To do a Range, you really only need a start and end value of a type that implements IComparable or IComparable.
You seem to be building a combination of Counter, Set, and Range.
The Range Pattern was very well documented in Fowler’s book, Analysis Patterns (if that interests you).
Yes, I’ve been coming to similar conclusions myself – although I still prefer using a Comparer to just demanding IComparable. It offers more flexibility.
My current plan is to have Range and RangeEnumerator (implementing IEnumerable, slightly annoyingly).
I’ll see if I can get hold of Analysis Patterns to see what Fowler reckons too :)