Five Great Patterns for Dealing with References

Five Great Patterns for Dealing with References

n a UML class model, classes are described by having attributes, operations, and relationships with other classes. When such a model is converted into code, both the public attributes and the relationships will appear as properties in your classes. These properties have types, which generally fall into one of the following categories:

  • Basic types. Simple properties such as FirstName, StartDate, or Limit have simple property types, such as string, datetime, or int.
  • Value types. Properties such as Email, ZIP, or CreditCard are a bit more sophisticated. Such properties tend to enforce validations on the possible values of properties, including regular expression and range checks. Therefore, types of such properties are described in value types, such as BankAccount, ZIP, or CreditCard.
  • Business class. Properties of classes that implement a relationship with another class in general have the type of that class, either representing a single instance or a collection of instances of the related class. A Project class, for instance, is likely to have a property Manager of type Employee.
  • Reference types. When the value of a property comes from a limited collection of values, the type of that property uses a reference type. There are several implementations for reference types, ranging from enumerated types, such as CourseLevel in a Course class to small referential classes, including a Country or Currencies class.

At first glimpse, enumerated types and referential classes appear to be to the most obvious patterns for implementing reference types, but there are other patterns that can do the job for you. This article includes five such patterns; I’ll show you when each of these patterns is applicable and useful.

Each pattern is measured up against a number of criteria:

  • Type safety. When referring to the property involved, is this type safe? When the type of the property is passed as an argument in an operation, can only values from the limited collection be used? Is it possible to type check values in code, without making the code vulnerable to typing errors? For instance
       if (MyCourse.CourseLevel == CourseLevels.Expert) { … }

    is not really equivalent to

       if (MyCourse.CourseLevel == "Expert") { … }

    The second example relies on perfect user input in order to be validated. But eventually, someone will make a typo.

  • Collection of values. When developing a user interface, quite often all possible values from the collection of the property’s type need to be displayed, for instance in drop-down lists or radio buttons. How much programming effort is involved in retrieving all possible values for a certain reference type?
  • Display values. In some cases, it is desirable to display different values than the technical values that are used in your code. For instance, an enumeration CourseLevel might define a value VeryHigh, but this might appear on screen as Very high.
  • Flexibility. Can the values of the elements in the collection change without changing or breaking the code? Is the number of elements fixed or can new elements be added without endangering the application code?
  • Extending reference types. In some cases, it is useful to define a referential type, and than later be able to inherit from it and extend it with new values. Suppose you’re using a framework that uses a reference type Actions that contains all actions the framework is aware of. Developers might need to use both existing functionality as well additional Actions that aren’t yet defined. They can get exactly what they need by extending Actions.

Now, let’s look at a number of different patterns for dealing with references, and monitor how these patterns behave to the aforementioned criteria.

Enumeration Pattern
When you have a fixed number of elements to choose from, an enumeration is a suitable pattern for implementing references. An enumeration is a hard-coded collection of values. The enumeration CourseLevels in the following code is an excellent example.

   public enum CourseLevels   {      Beginner,      Advanced,      VeryHigh,      Expert   }

Using an enumeration is easy, as is shown in the next code example.

   public class Course   {      …      private CourseLevels level = CourseLevels.Beginner;      public CourseLevels Level      {         get { return level; }         set { level = value; }      }   }

Enumeration is a very elegant pattern, when the property’s value needs to be checked in your application code, as the following code example shows.

   if (MyCourse.Level == CourseLevels.Beginner) { … }

Methods on the Course class can be specified elegantly as follows.

   public bool HasLevel(CourseLevels level)   {     return (Level == level);   }

It is also easy to retrieve all possible values of an enumeration. In C# a simple call to CourseLevel.GetNames() or CourseLevel.GetValues() will do. Although using an enumeration is very straightforward, the pattern has a few issues. When its values need to be displayed on screen, there is no possibility of using different values than the ones defined by the enumeration. For sure, VeryHigh will appear as VeryHigh in your drop-down list.

Furthermore, if somehow the value of an element needs to be changed or new elements need to be introduced, enumerations lack the flexibility to perform this operation without recompiling the application. And third, in .NET enumerations are implemented as value type, which means that there is no way of extending them.

Constant Collection Pattern
To get around this third issue, developers tend to use a second pattern for using references, which I call the constant collection. In this pattern the collection of possible values is implemented as a class with a number of constants defined as its fields, much like in the following code example. Such a class can easily be inherited from.

   public class CourseLevels   {      public const string Beginner = "Beginner";      public const string Advanced = "Advanced";      public const string VeryHigh = "Very high";      public const string Expert = "Expert";   }

The constant collection pattern also alleviates the second issued raised by the enumeration pattern. When displaying values on screen the actual value of the constants can be used, thus allowing for VeryHigh to be displayed as Very high. However, in return, the constant collection pattern raises its own issues. First of all, one can only retrieve all possible values using reflection. This may not be very desirable.

Worse, constant collections are not always type safe, even if they appear to be at first sight. Because the constants are defined static, the constant collection is type safe in respect to specific checks, like this one:

   if (MyCourse.Level == CourseLevels.Beginner) { … }

But beware: Methods on the Course class will have to be specified with a signature as follows.

   public bool HasLevel(string level)   {      return (Level == level);   }

Thus, there is no guarantee that only type-safe values are passed, as with an enumeration. Both statements in the following code example will return true when MyCourse has level Advanced. And even worse, the third statement will not produce a compiler error, as one would hope, since Easy is not a valid value.

   bool hasLevel = MyCourse.HasLevel(CourseLevels.Advanced);   bool hasLevel = MyCourse.HasLevel("Advanced");   bool hasLevel = MyCourse.HasLevel("Easy");

Descriptor Pattern
The third pattern, the descriptor pattern, mixes best practices from the previous two patterns. Basically, a descriptor is a collection of instances of the class itself. Using this pattern, the CourseLevels class can be implemented as follows.

   public class CourseLevels   {      public static CourseLevels Beginner = new CourseLevels();      public static CourseLevels Advanced = new CourseLevels();      public static CourseLevels VeryHigh = new CourseLevels();      public static CourseLevels Expert = new CourseLevels();   }

Because the instances are again static, descriptors are type safe, both in simple checks as in operation signatures, just like enumerations, as the following example shows.

   if (MyCourse.Level == CourseLevels.Beginner) { … }

Methods on the Course class can also be specified elegantly.

   public bool HasLevel(CourseLevels level)   {     return (Level == level);   }

Also descriptors can be extended, like constant collections. In the simple implementation above however, it is not possible to use display values other than the defined names of the instances. Luckily a straightforward extension to this implementation solves these issues, if necessary.

   public class CourseLevels   {      public string DisplayValue;      public CourseLevels(string displayvalue)      {         DisplayValue = display;      }      public static CourseLevels Beginner = new CourseLevels("Beginner");      public static CourseLevels Advanced = new CourseLevels("Advanced");      public static CourseLevels VeryHigh = new CourseLevels("Very high");      public static CourseLevels Expert = new CourseLevels("Expert");   }

Still, descriptors have a fixed number of elements, just like enumerations and constant collections. And again, like the latter, retrieving the set of possible values requires reflection. In C#, in both patterns, an operation like the following takes care of this job.

   public static CourseLevels[] GetValues()   {      ArrayList list = new ArrayList();      foreach (FieldInfo pi in type.GetFields())      {         list.Add((CourseLevels) pi.GetValue(type));      }      return (CourseLevels[]) list.ToArray(typeof(CourseLevels));   }

Type Safe or Flexible?
So far, the patterns introduced are very useful when it comes to type safety. To enable this safety net the specific reference elements in these patterns are hard coded. However, situations may occur where type safety is less important than having the flexibility to change and add new reference elements without having to change the code. A simple example for this is reference types such as Countries or Currencies. Although the number of countries is not subject to many changes, whenever a new country emerges, it needs to be added immediately to the collection of elements. Thus, a second species of reference patterns arises, in which flexibility prevails security.

Small Business Class Pattern
Suppose you have a Location class that specifies a location where courses are held. Such a class would have a property Country that refers to a specific instance of a reference type Countries, such as in the next code example.

   public class Location   {      public string Name;      public string Address;      public string City;      public Countries Country;   }

In this implementation of the Location class the property Country can be set to any valid instance of the Countries class. The Countries class is a small business class. New elements are hardly ever entered, existing elements rarely change and small business classes are mostly used as references. In most cases, the data of instances of small business classes are kept in a database, each small business class having an associated table in the database. Regardless of how the data is retrieved from the database, a small business class may look something like this.

   public class Countries   {      public string Name;      public string Description;      public string CountryCode;      …      public static Countries New()      { … }      public static Countries Get(ID id)      { … }      public bool Delete()      { … }      public bool Update()      { … }      public static Countries[] All      {         get { … }      }   }

It goes without saying that display values for small business class can be combined from any of the available fields. Preferably, you could override the ToString() operation, and use this.

   public class Countries   {      …      public string ToString()      {         return Name + “ (“ + Code + “)”;      }   }

Note that in the database, each record in the Location table holds a foreign key to an associated primary key in the Countries table. Using a small business classes as reference allows for great flexibility. That is, it is easy to change or add an element, without having to recompile the application. Another benefit of using small business classes is that retrieving the collection of all instances is straightforward. It is not unlikely that a small business class holds a static property All (or likewise operation) that returns the collection of all countries, as an array of instances of Countries. And more, it is easy to inherit from a small business class and add additional functionality.

Using a small business class also has some drawbacks, especially when type-safe validations needs to be performed on specific fields of the class, like in the following code example from the Location class. In this example, a string is used for implementing the validation, again allowing typing errors?recall the constant collection.

   public class Location   {      …      private Countries country;      public Countries Country      {         get { return country; }         set { if (value.CountryCode != “NL”) country = value; }      }   }

If type safety is required, the Countries class can be extended with a number of static instances that can be used in validations. In the following code example the instance Netherlands is predefined.

   public class Countries   {      …      public static Countries Netherlands = Countries.Get("NL");   }

Now validation can be done using this static instance, hence it has the same syntax as validation using an enumeration or descriptor.

   public class Location   {      …      private Countries country;      public Countries Country      {         get { return country; }         set { if (value != Countries.Netherlands) country = value; }      }   }

Note that, since the instance Netherlands is declared static, the associated record will only be retrieved once from the database?when the first validation is executed. This extension technique is only useful when only a limited number of validations need to be performed. And although it offers more possibilities for static checking, it diminishes the flexibility of the solution, because these static instances may never be removed from the underlying table.

There’s always a trade-off between usability (in code) and flexibility (in use). You’ll find this trade-off is the main criteria when choosing one of these patterns.

Smart Reference Pattern
In the last pattern, a single small business class is defined. This single small business class is used to refer to any reference type that can be expressed in the properties of that class. Typically such a class, called a smart reference, has properties such as Name and Description. The single-most important property of a smart reference however, is the Type property, which declares which specific reference type is meant.

The Type property can best be expressed using an enumeration, for instance called ReferenceTypes, as in the following code example. Using this property, operations such as All() and Default() can be defined for retrieving all instances of SmartReference for a specific type, or even the default instance. Such operations can be declared both static and not static, either specifying the type, or using the instance’s Type property.

   public enum ReferenceTypes   {      Countries,      Currencies   }      public class SmartReference   {      public string Name;      public string Description;      public ReferenceTypes Type;      …      public SmartReference[] All()      { … }      public static SmartReference[] All(ReferenceTypes rt)      { … }   }

This last pattern is extremely flexible. New elements can easily be added to any of the types in the ReferenceTypes enumeration. The main benefit of using a smart reference over a number of small business classes is that it saves you a lot of programming effort. Maintenance functionality for all your reference types consists only of a single form, allowing the user to select a reference type and maintain the elements of that type.

However there is a small price to pay for this maintenance flexibility. Using a smart reference does not automatically ensure type-safe validation. A property of type SmartReference will need to perform additional reference type validation, as demonstrated here:

   public class Location   {      …      private SmartReference country;      public SmartReference Country      {         get { return country; }         set { if (value.Type == ReferenceTypes.Country) country = value; }      }   }

Also the display value for a SmartReference is limited to the fields defined by the SmartReference class. Again, I suggest using the ToString() operation to set the display value to any combination of fields.

When to Use Which Pattern?
Using references is inevitable when using business classes. However, there a lot of different implementations, all of which are variations or extensions to one of the five patterns mentioned above. The big issue is when to use which pattern. Table 1 sums up the characteristics for each of the patterns using the criteria defined earlier.

Table 1. Characteristics of each of five different patterns and their key criteria.

Type safetyCollectionsof valuesDisplay valuesFlexibilityExtensibility(inheritance)
EnumerationYesYesLimited to enumerated valuesFixed, change of code requiredNo
Constant collectionNot when used as parameterYes, using reflectionLimited to defined valuesFixed, change of code requiredYes
DescriptorYesYes, using reflectionYesFixed, change of code requiredYes
Small business classYes, when defining static instancesYes, easily retrieved from databaseAny combination of its fieldsFlexible, alter dedicated tableYes
Smart referenceNo, anonymousYes, easily retrieved from databaseAny combination of its fieldsFlexible, alter single tableYes

The first three patterns are best in situations where a limited and fixed number of elements are expected. The three patterns mainly differ in that different display values can be used or where extensibility by inheritance is required. The last two patterns are best used in situations where there is a large collection of elements, and this collection is unlikely to change more often than the code is put in production. This requires the elements to be stored externally, rather than being hard coded, most commonly in a database.

Together, these five patterns, and the numerous variations and extensions to them, will give you control over the references you need to deal with in your projects.

See also  Strengthening Business Cybersecurity With Threat Intelligence

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist