Browse DevX
Sign up for e-mail newsletters from DevX


Build a Generic Range Class with .NET 2.0 : Page 4

Find out how to take advantage of the generics capability introduced with .NET 2.0, which provides an elegant solution to performing range checks within your applications.




Building the Right Environment to Support AI, Machine Learning and Deep Learning

Who Said Constraints Were A Bad Thing?
Thankfully, the designers of generics were two steps ahead of us. And they realized that sometimes you would need to expect a certain level of functionality from the types that consume your generic classes. In .NET 2.0, you accomplish this using generic constraints. The limited number of methods at your disposal on the T arguments in the range class provide no way to perform any comparison operation between two T types. But you can constrain the possible types to those capable of making such comparisons. Make the following small change to your IRange interface:

public interface IRange<T> where T : IComparable<T>

The generic interface now makes use of an interface derivation constraint. This tells the compiler that the only types that can satisfy T must implement IComparable<T>. In other words, you can compare any valid type T to other valid T types? Fortunately, most of the value types in the framework (int, decimal, double etc) implement IComparable<T> for their respective types.

Figure 3. IComparable.CompareTo Return Values: This table shows the valid return values and their meanings from calling the CompareTo method of an object that implements the IComparable interface.
You will need to duplicate the constraint in all implementors of the IRange<T> interface (currently the Range<T> class). Unfortunately, in the current generics implementation, you can't simply define constraints on the interface, they must be placed on both the interface and on any interface implementations.

Let's get back to getting the failing non-compiling test to pass. You now know that all valid types must support IComparable<T> operations against other T's. The IComparable<T> interface has one method:

Int CompareTo (T other)

Chances are you have already made use of the non-generic IComparable interface in prior versions of the framework. The table in Figure 3 (from the MSDN documentation) details the return values of the method:

Armed with that knowledge you can complete the constructor code for the Range<T> class.

public Range(T start, T end) { if (start.CompareTo(end) <=0) { this.start = start; this.end = end; } else { this.start = end; this.end = start; } }

Using the interface derivation constraint lets you invoke any method exposed by the IComparable<T> interface on the T generic type parameter (in other words, CompareTo). You can use this to check whether the values coming into the constructor need to be rearranged before assignment to the private fields of the class.

What's In A Range!
So far, this is a fairly non-functional Range. You need to beef it up with its most important method—asking it whether it contains a particular value. Write a test to describe your intention:

[Test] public void ShouldBeAbleToTestForTheExistenceOfANumberInARange() { IRange<int> rangeOfIntegers = new Range<int>(20, 30); Assert.IsFalse(rangeOfIntegers.Contains(19)); for (int i = 20; i <= 30; i++) { Assert.IsTrue(rangeOfIntegers.Contains(i)); } Assert.IsFalse(rangeOfIntegers.Contains(31)); }

Those of you coding along will once again be in non-compilable state, because the Range does not expose a Contains method. Get the code to compile by adding the method with no functionality. Remember, you are making use of interfaces, so add the method to the IRange<T> interface first. Then you can add the unimplemented method to the Range class.

public interface IRange<T> where T : IComparable<T> { T Start{get;} T End{get;} bool Contains(T valueToFind); }

Then, in the Range class, add the method:

public bool Contains(T valueToFind) { throw new NotImplementedException(); }

Now the unit test should fail with a NotImplementedException. To implement the method you once again can make use of the fact that you can call CompareTo on the valueToFind parameter, so the resulting code is very simple:

public bool Contains(T valueToFind) { return valueToFind.CompareTo(Start) >= 0 && valueToFind.CompareTo(End) <= 0; }

The Contains method implementation plainly shows the range-checking code that you often see scattered throughout applications. But now, using the Range<T> class, that code is nicely encapsulated in one place.

One Range To Rule Them All!
You need to prove that you now truly have a Range class that will work will "almost" any type. I say almost because the derivation constraint limits the class to types that implement IComparable<T>. That's not a large constraint because most if not all the common value types in the framework implement IComparable<T>. And let's be honest, implementing IComparable<T> for custom types is definitely not rocket science. Listing 2 shows the use of our Range<T> class to perform Range checking with dates, which is a problem often encountered in myriads of applications.

Although you've been working with integers throughout the process of creating the Range class, you should notice that you didn't need to alter the class or create any new classes to get the test for a date range to pass! I could carry on and demonstrate the functionality against other types in the framework, or even other custom types that implement IComparable<T>, but I think you can see the point. The source code that accompanies this article also contains a rewrite of the Discount class that makes use of an IRange<T> internally to perform discount applicability checking.

The generic range class discussed in this article really just scratches the surface of the functionality that you can bundle into a range implementation. You now have the tools and knowledge to build a full-blown implementation of the Range pattern such as Martin discussses in this section of his Analysis Patterns book. You are also now armed with a bit more knowledge that you can use to apply generics pragmatically in your own applications. And I hope that you won't have to code an explicit Range check for a long time!

Jean-Paul S. Boodhoo is a .NET delivery expert who has been working with the .NET Framework since beta 1 of .NET 1.0. He has over seven years of experience in architecting, designing, and developing applications. He is as an independent consultant who helps teams realize success through agile practices and pragmatic Behavior Driven Development (BDD) techniques. He has written articles for Visual Studio Magazine, DevX, and MSDN that use BDD to apply .NET pragmatically. Jean-Paul has presented on the popular podcast/screencast .NET Rocks! and DNRTV and has delivered Webcasts for Microsoft on the topic of design patterns in the real world. He is a member of the MSDN Canada Speakers Bureau and a Microsoft Most Valuable Professional (MVP). He makes continual efforts to update his blog.
Thanks for your registration, follow us on our social networks to keep up-to-date