Build a Generic Range Class with .NET 2.0

Build a Generic Range Class with .NET 2.0

am a big fan of objects. It’s so convenient to work with programming entities that you can ask to do something and they just do it. The shift that we developers have made from procedural style to OO style allows us to express ideas in much richer ways than were possible using procedural methods. Knowing how to identify areas where a new object can be introduced to simplify the code in your application is a talent that many spend years improving (I can’t say perfect, because software development is truly an evolutionary process).

How many times in your applications do you write logic that deals with ranges of values? For example, consider an order entry system that allows “special” orders to be processed only on certain weekdays?and then only between certain hours. Systems such as this make heavy use of range checks to ensure that accepted orders meet validity constraints. It’s not uncommon to see code such as this, which is an NUnit test that checks whether a date lies within a specified range:

   [Test]   public void ShouldBeAbleToSeeIfDateExistsInRange()   {      DateTime startOfJanuary2005 = new          DateTime(2005,1,1);      DateTime endOfJanuary2005 = new          DateTime(2005,1,31);      DateTime middleOfJanuary2005 = new          DateTime(2005,1,15);      DateTime startOfFebruary2005 = new          DateTime(2005,2,1);                     Assert.IsTrue(middleOfJanuary2005 >=          startOfJanuary2005 &&          middleOfJanuary2005 <= endOfJanuary2005);      Assert.IsFalse(startOfFebruary2005 >=          startOfJanuary2005 &&          startOfFebruary2005 <= endOfJanuary2005);   }

Often you'll find this type of range-checking code scattered throughout multiple parts of an application. Of course, these checks are usually not localized to one specific type; sometimes you may want to perform range checks on types other than dates. In the context of an order entry application, there may be times when you want to calculate an order discount based on the product amount purchased in a particular order. The following code shows a test for a class that encapsulates applicability for discounts.

   [Test]   public void ShouldTestForEligibilityOfDiscount()   {      IDiscount between100And300DollarDiscount =          new DiscountWithoutRange(100, 300, .10m);      Assert.IsFalse(between100And300DollarDiscount.         IsApplicableTo(99));      Assert.IsTrue(between100And300DollarDiscount.         IsApplicableTo(100));      Assert.IsTrue(between100And300DollarDiscount.         IsApplicableTo(205));      Assert.IsTrue(between100And300DollarDiscount.         IsApplicableTo(300));      Assert.IsFalse(between100And300DollarDiscount.         IsApplicableTo(301));   }

The test is fairly self-explanatory. After creating a DiscountWithoutGenerics object, you can ask if it can be applied to a particular order amount. Listing 1 shows the full implementation of the DiscountWithoutGenerics class. Here's the code that actually performs the range check:

   return orderAmount >= eligibleAmountStart &&       orderAmount <= eligibleAmountEnd;

Notice that other than the type (Decimal vs. DateTime) the logic for determining whether a value exists within a range is the same as the date example; in other words, it answers the question: "Does the value I care about fall between the starting and ending values of the range?"

There Must Be A Better Way
One great thing about code is that if you see something you don't like, you can refactor it. Range-checking in applications is a prime candidate for refactoring. Martin Fowler originally introduced the concept of a Range pattern in this section of his book "Analysis Patterns." He described a Range object as "a single object to represent the range as a whole, [which] then provides the relevant operations to test to see if values fall in the range and to compare ranges."

The DiscountWithoutGenerics class fits that description of a Range. It encapsulates the logic required to determine if a value (the order amount) falls within the range of values for which the discount is applicable. Clients of the DiscountWithoutGenerics class don't need to perform any explicit boundary checks; those are all encapsulated within the "Range" class itself.

While using this pattern is a great way to encapsulate Range checks within your applications, it does have one big flaw: Right now range-checking is localized to a very specific class of the application. Worse, the DiscountWithoutGenerics class can work only with decimal values.

Ultimately the basic operations on any Range are the same:

  • What is the start value?
  • What is the end value?
  • Does it contain "this" value?

To make the effort of building a Range class truly worthwhile, you need a way to create a Range class that can create ranges of any type. In other words, you should be able to create one Range class that can work with with ints, doubles, decimals, dates etc, and even our own custom types. The rest of this article discusses how to use Test-driven Development (TDD) to build a Range class that you can use with any type.

Building The Range Class
The key to creating a class that will be able to work with any type that you wish lies in the new generic language enhancements added to C# (and VB.NET) in .NET 2.0. Most people who have seen examples of the new collection classes in .NET have already had some exposure to the new generic language enhancements. Remember back to the ancient days of .NET 1.1, where if you needed a custom collection of "Order" objects you would have to create your own custom collection class?and you would usually end up using an ArrayList as a private backing store. The purpose of creating the custom collection class was to ensure type safety, not to provide people with the ability to add anything they wanted to your private collection. With .NET 2.0 and generics you can achieve the same result with much less code:

   IList orders = new List();   orders.Add(new Order());   

Take a look at the C# definition for the generic IList interface:

   public interface IList : ICollection,       IEnumerable, IEnumerable

I should take a quick second to point out that there is no special significance to the use of the letter "T." The compiler doesn't enforce any particular naming convention for generic type parameters. The designers of the framework could just have easily defined the interface as follows:

   IList : ICollection,      IEnumerable, IEnumerable   

What's important to remember is that the T or SomeItem is a placeholder for a type. Notice all the T parameters in the interface definition. This is the magic that allows the generic IList interface to work with any type. When you want to construct an implementation of a generic IList (the most common implementation being List), you have to provide it with a type to use in place of the generic type parameter T. The previous code segment constructed a List of that constrained all the methods so they accept only Order types. The creators of the generic List class didn't actually care what type T was, leaving it up to the class consumer to decide what type the List should actually work with.

This is the same concept that you will have to apply to the formation of a generic Range class. The Range should be flexible enough to work with any type. Start by writing a test to see if you can create a Range of integers:

   [Test]   public void ShouldCreateARangeOfIntegers()   {     IRange rangeOfIntegers = new         Range(20, 30);     Assert.AreEqual(20,range.Start);     Assert.AreEqual(30,range.End);   }

When you write code from a TDD perspective, try to write from the point of view of a client programmer utilizing your library. Ask yourself "What is the ideal way for a client to consume and work with this object?" You then express your expectations in code as a test; get the test compiling, get the test passing, and then cleanup any mess you may have created in the process. If you are following along and coding with me, then your code will currently be in a non-compilable state. First, get the test to compile by adding the required classes and interfaces shown below:

   public interface IRange   {      T Start{get;}      T End{get;}   }   public class Range : IRange   {      public Range(T start, T end)      {         throw new NotImplementedException();      }            public T Start      {         get { throw new NotImplementedException(); }      }         public T End      {         get { throw new NotImplementedException(); }      }   }

Note that you've implicitly made some pretty big design decisions by writing this one test.

  • The Range class will implement an IRange interface.
  • The Range class will have one constructor that takes the start and end values of the range.
  • The Range class will expose its start and end values as properties.

Coding to the Test
For the most part, the current implementation of the interface and class are very simple?sufficient to get the code to compile. But if you run the test right now it should fail. That's the point however! So far, you've created a generic interface called IRange that requires one generic type argument?the type for which you want to create the range. The Range class implements the IRange interface, satisfying the IRange interface argument using the type argument that a client provides upon construction. Take a look at the arguments to the constructor:

   public Range(T start, T end)   {     throw new NotImplementedException();   }   

Remember, you don't know what types the clients are going to want to use the Range class for, so you must substitute the generic type parameter (T) wherever you need to use the actual type the range is created for. The constructor tells clients that they must provide a new Range with a start value and an end value. For example to create a IRange of integers you would do the following:

   IRange rangeOfIntegers = new Range(20, 30);   

Note that when you construct a Range that you can pass only integer values to the constructor. Any method in the class that accepts or returns T is now strongly typed to the type provided to the constructor as a generic argument (in this case integers).

Let's get back to making the test pass. Update the code in the Range class to match the code below. After doing that, it should pass the test.

   public class Range : IRange   {     private readonly T start;     private readonly T end;        public Range(T start, T end)     {        this.start = start;        this.end = end;                 }        public T Start     {        get { return this.start; }     }        public T End     {        get { return this.end; }     }   }

Now you can add a second test for the integer range:

Figure 1. NUnit Test Failure: The figure shows the NUnit failure output caused by running the new Range test.
    [Test]   public void       ShouldCreateRangeWithStartAndEndAssignedCorrectly()   {     IRange rangeOfIntegers = new         Range(30, 20);     Assert.AreEqual(20,rangeOfIntegers.Start);     Assert.AreEqual(30,rangeOfIntegers.End);   }   

This test ensures that the Range will assign the correct values to the start and end of the range, even if the client of the Range provides it with mixed up values upon construction. Figure 1 shows the NUnit failure caused by the running the new test:

The test fails because the Range does not yet implement the logic to correctly rearrange and assign values passed to the constructor. You would logically think that you could implement the assignment code using the following logic:

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

However, if you write the above code you will quickly see that it does not compile but gives you the following error message:

Figure 2. Non-specific Generic Type Methods: The four methods shown are the only methods available to all generic types.
   "Cannot apply operator '<=' to operands of type     'T' and 'T' ".

Why are you getting this error? Remember, you are coding your Range class so that it can work with any type. This actually raises a problem. How can you be sure that all types that would want to consume your class actually provide a <= operator. The answer is: You can't. This puts you in an interesting predicament. Figure 2 shows the methods at your disposal when accessing arguments of type T?and that's it. In other words, you have nothing more to work with than the same methods available to all descendants of Object (which is every class in the framework).

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 where T : IComparable   

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. 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 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 interface (currently the Range 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 operations against other T's. The IComparable 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 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 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 rangeOfIntegers = new         Range(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 interface first. Then you can add the unimplemented method to the Range class.

   public interface IRange where T : IComparable   {     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 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. That's not a large constraint because most if not all the common value types in the framework implement IComparable. And let's be honest, implementing IComparable for custom types is definitely not rocket science. Listing 2 shows the use of our Range 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, 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 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!



Share the Post:
Advanced Drones Race

Pentagon’s Bold Race for Advanced Drones

The Pentagon has recently unveiled its ambitious strategy to acquire thousands of sophisticated drones within the next two years. This decision comes in response to

Important Updates

You Need to See the New Microsoft Updates

Microsoft has recently announced a series of new features and updates across their applications, including Outlook, Microsoft Teams, and SharePoint. These new developments are centered

Price Wars

Inside Hyundai and Kia’s Price Wars

South Korean automakers Hyundai and Kia are cutting the prices on a number of their electric vehicles (EVs) in response to growing price competition within

Solar Frenzy Surprises

Solar Subsidy in Germany Causes Frenzy

In a shocking turn of events, the German national KfW bank was forced to discontinue its home solar power subsidy program for charging electric vehicles

Advanced Drones Race

Pentagon’s Bold Race for Advanced Drones

The Pentagon has recently unveiled its ambitious strategy to acquire thousands of sophisticated drones within the next two years. This decision comes in response to Russia’s rapid utilization of airborne

Important Updates

You Need to See the New Microsoft Updates

Microsoft has recently announced a series of new features and updates across their applications, including Outlook, Microsoft Teams, and SharePoint. These new developments are centered around improving user experience, streamlining

Price Wars

Inside Hyundai and Kia’s Price Wars

South Korean automakers Hyundai and Kia are cutting the prices on a number of their electric vehicles (EVs) in response to growing price competition within the South Korean market. Many

Solar Frenzy Surprises

Solar Subsidy in Germany Causes Frenzy

In a shocking turn of events, the German national KfW bank was forced to discontinue its home solar power subsidy program for charging electric vehicles (EVs) after just one day,

Electric Spare

Electric Cars Ditch Spare Tires for Efficiency

Ira Newlander from West Los Angeles is thinking about trading in his old Ford Explorer for a contemporary hybrid or electric vehicle. However, he has observed that the majority of

Solar Geoengineering Impacts

Unraveling Solar Geoengineering’s Hidden Impacts

As we continue to face the repercussions of climate change, scientists and experts seek innovative ways to mitigate its impacts. Solar geoengineering (SG), a technique involving the distribution of aerosols

Razer Discount

Unbelievable Razer Blade 17 Discount

On September 24, 2023, it was reported that Razer, a popular brand in the premium gaming laptop industry, is offering an exceptional deal on their Razer Blade 17 model. Typically

Innovation Ignition

New Fintech Innovation Ignites Change

The fintech sector continues to attract substantial interest, as demonstrated by a dedicated fintech stage at a recent event featuring panel discussions and informal conversations with industry professionals. The gathering,

Import Easing

Easing Import Rules for Big Tech

India has chosen to ease its proposed restrictions on imports of laptops, tablets, and other IT hardware, allowing manufacturers like Apple Inc., HP Inc., and Dell Technologies Inc. more time

Semiconductor Stock Plummet

Dramatic Downturn in Semiconductor Stocks Looms

Recent events show that the S&P Semiconductors Select Industry Index seems to be experiencing a downturn, which could result in a decline in semiconductor stocks. Known as a key indicator

Anthropic Investment

Amazon’s Bold Anthropic Investment

On Monday, Amazon announced its plan to invest up to $4 billion in the AI firm Anthropic, acquiring a minority stake in the process. This decision demonstrates Amazon’s commitment to

AI Experts Get Hired

Tech Industry Rehiring Wave: AI Experts Wanted

A few months ago, Big Tech companies were downsizing their workforce, but currently, many are considering rehiring some of these employees, especially in popular fields such as artificial intelligence. The

Lagos Migration

Middle-Class Migration: Undermining Democracy?

As the middle class in Lagos, Nigeria, increasingly migrates to private communities, a PhD scholar from a leading technology institute has been investigating the impact of this development on democratic

AI Software Development

ChatGPT is Now Making Video Games

Pietro Schirano’s foray into using ChatGPT, an AI tool for programming, has opened up new vistas in game and software development. As design lead at business finance firm Brex, Schirano

Llama Codebot

Developers! Here’s Your Chatbot

Meta Platforms has recently unveiled Code Llama, a free chatbot designed to aid developers in crafting coding scripts. This large language model (LLM), developed using Meta’s Llama 2 model, serves

Tech Layoffs

Unraveling the Tech Sector’s Historic Job Losses

Throughout 2023, the tech sector has experienced a record-breaking number of job losses, impacting tens of thousands of workers across various companies, including well-established corporations and emerging startups in areas

Chinese 5G Limitation

Germany Considers Limiting Chinese 5G Tech

A recent report has put forth the possibility that Germany’s Federal Ministry of the Interior and Community may consider limiting the use of Chinese 5G technology by local network providers

Modern Warfare

The Barak Tank is Transforming Modern Warfare

The Barak tank is a groundbreaking addition to the Israeli Defense Forces’ arsenal, significantly enhancing their combat capabilities. This AI-powered military vehicle is expected to transform the way modern warfare

AI Cheating Growth

AI Plagiarism Challenges Shake Academic Integrity

As generative AI technologies like ChatGPT become increasingly prevalent among students and raise concerns about widespread cheating, prominent universities have halted their use of AI detection software, such as Turnitin’s

US Commitment

US Approves Sustainable Battery Research

The US Department of Energy has revealed a $325 million commitment in the research of innovative battery types, designed to enable solar and wind power as continuous, 24-hour energy sources.

Netanyahu Musk AI

Netanyahu and Musk Discuss AI Future

On September 22, 2023, Israeli Prime Minister Benjamin Netanyahu met with entrepreneur Elon Musk in San Francisco prior to attending the United Nations. In a live-streamed discussion, Netanyahu lauded Musk

Urban Gardening

Creating Thriving Cities Through Urban Gardening

The rising popularity of urban gardening is receiving increased recognition for its numerous advantages, as demonstrated in a recent study featured in the Environmental Research Letters journal. Carried out by