C# Coding Solutions—Comparer Functor
Microsoft .NET Framework, ASP.NET, Visual C# (CSharp, C Sharp, C-Sharp) Developer Training, Visual Studio
The Comparer Functor
DelegateComparer is not a static implementation and can compare different types according to different criteria. DelegateComparer could validate if the age of a person is greater than another person. These types of sorting, arranging, or filtering are typically implemented as unique collection types: LinkedList, Stack, Queue, etc. A functor can offer more functionality, but uses standard collections types, as Figure 4-3 shows.
Figure 4-3. UML functor architecture for collections
The interface IList<> is the base interface for defining all functor proxies. IList<> extends the ICollection<>, IEnumerable<>, and IEnumerable interfaces. The class BaseProxy<> is an abstract base class that provides a default implementation for the IList<> interface. The default action for BaseProxy<> is to proxy all requests to the parent ICollection<> implementation. The classes TransformerProxy<>, PredicateProxy<>, ComparerProxy<>, and ClosureProxy<> represent the implementations of the various functors.
When defining a proxy that uses functors, the prime consideration is what functionality the functor will override. A collection has many operations, such as adding, deleting, and retrieving elements. A very popular use of functors is to execute a functor when adding elements to a collection. However, nothing says that a functor cannot override the remove-element operations. However, it is very important never to create proxies that will call a functor when adding and removing elements. This is because the functors’ default method signature does not know what the operation is. The preferred approach is to create a proxy for each grouping of operations, and then combine multiple proxies.
You may notice that combining a functor with Proxy pattern results in a Decorator pattern. The architecture is very similar to a Decorator pattern. What is different is how the interfaces are instantiated and manipulated. In the case of the functor and the Proxy pattern, the client has no direct access to the underlying parent collection. The Proxy pattern controls all access.
In the Decorator pattern a client can access any element in the chain of elements created by the Decorator pattern. And more often than not, the Decorator pattern decorates a single method or subset of the full functionality offered by the implementation. So in a nutshell the difference between a Proxy and a Decorator pattern is that the Proxy pattern controls all access to the underlying implementation, and the Decorator pattern does not.
The remainder of this section will illustrate the four functors and present a small example to wrap everything together. You can use the predefined IComparable interface from the .NET Framework to define a comparer functor; the definition is illustrated as follows:
public interface IComparable { int CompareTo(object obj); }
The IComparable interface has a single method, CompareTo, that represents the object instance to compare to. The IComparable interface works if the object that performs the testing implements the IComparable interface. The problem with this approach is if the objects to be compared do not have IComparable implemented, then the comparable functor will not work. As in the Decorator pattern, all implementations concerned have to cooperate.
From the .NET base classes you can use another interface to compare two objects; IComparer is defined as follows:
public interface IComparer { int Compare(object x, object y); }
IComparer has a single method that has two parameters representing the objects to be compared. The advantage of the IComparer interface is that the objects to be tested do not have to implement any additional functionality.
For illustration purposes I will use a delegate, not an interface. A delegate can be implemented using C# 2.0 anonymous methods, or any method of a class, whereas an interface must be implemented and then instantiated. If necessary, the Adapter pattern can be applied to convert the delegate into an interface. The IComparer interface, defined in terms of a delegate, is as follows:
public delegate int DelegateComparer< type1, type2> ( type1 obj1, type2 obj2);
The delegate DelegateComparer<> is defined using a template, making it type-safe, and returns an int value indicating the results of a comparison. The two parameters are the object instances to compare.
The definition of a flight ticket illustrates a use of the comparer delegate. The flight ticket will be defined in terms of flight legs that are wired together like a linked list. The individual flight legs will not be managed using a collection type. I’ve explicitly not used a collection to illustrate how functors can be used on types that do not include collections. The following interface definition defines one leg of a flight:
interface IFlight { string Origin { get; set; } string Destination { get; set;} IFlight NextLeg { get; set; } }
The interface IFlight has three properties, where two (Origin, Destination) are strings and one (NextLeg) is a reference to IFlight. To wire two flight legs together, one instance has its NextLeg property assigned to the other instance. The Comparer functor is important for stopping a consumer from assigning the same flight leg twice.
The following is an implementation of the FlightComparer class (note that the implementations of the Origin and String properties have been omitted for clarity):
class FlightComparer : IFlight { IFlight _parent; DelegateComparer< IFlight, IFlight> _delegateComparer; public FlightComparer( IFlight parent, DelegateComparer< IFlight, IFlight> delg) { _delegateComparer = delg; _parent = parent; } public IFlight NextLeg { get { return _parent.NextLeg; } set { if( _delegateComper( _parent, value) != 0) { _parent.NextLeg = value; } else { throw new ComparerEvaluationException(); } } } }
The constructor of FlightComparer requires two parents: the parent IFlight implementation and a DelegateComparer delegate implementation. In the property NextLeg the get part calls the delegate _delegateComparer. If the delegate returns a nonzero value, the _parent.NextLeg data member can be assigned; otherwise an exception is generated.
This example illustrates how the logic of validation is not part of the original class. Validation has been removed from the Flight class, and if Flight is instantiated then an inconsistent hierarchy can be created since there is no validation. But this is very incorrect. Remember that Flight implements an interface, and that the interface is defined as public scope, whereas the implementation is internal scope. Therefore a factory is required to instantiate Flight. The factory can implement the Builder pattern that instantiates both classes, as illustrated by the following example:
IFlight FlightBuilder() { return new FlightComparer( new Flight(), delegate( IFlight flight1, IFlight flight2) { if( flight1.Equals( flight2)) { return 0; } else { return -1; } }); }
The method FlightBuilder instantiates the type FlightComparer and passes in a new Flight instance as the first parameter. What is unique is the second parameter that expects a delegate. Instead of declaring a class with a method, an anonymous method is created, which performs the validation. An anonymous method is ideal because functors will be defined as delegates. The returned IFlight instance will have a correct hierarchy, and since FlightComparer does not allow access to the Flight instance there is no chance that the client can bypass FlightComparer and corrupt the data.
If, however, you’re still uncomfortable with the use of the Proxy pattern, you can use the Flight class and have the property NextLeg use the delegate DelegateComparer directly. A proxy is advised because it makes it simpler to maintain and extend an application. From the client perspective the difference between the Factory and the Builder pattern is zero, so using a proxy is entirely acceptable.
The functor architecture looks more complicated than the following implementation, which is similar:
class Flight : IFlight { IFlight _nextLeg; public IFlight NextLeg { get { return _nextLeg; } set { if( !value.Equals( this)) { _nextLeg = value; } } } }
In the set part of the property the method value.Equals is called. In the example, the return value from value.Equals should be a comparison of the data members’ values. The value returned from value.Equals in this case is incorrect because the comparison is based on the reference information, not the data members of Flight. To have Equals return a correct value the Equals method has to be implemented. And if it is implemented, the GetHashCode method also must be implemented.
The return value of the method GetHashCode uniquely identifies an object with a certain state. If two different object instances contained the same state, then the GetHashCode method would return the same value. This carries over into the Equals method in that two objects that contain the same state are equal to each other.
What happens if the implementation of IFlight changes? The IFlight interface stored the legs using a linked list. If at some point a collection is used, the validation code remains identical but needs to be moved from one class to another. This involves a copy-and-paste operation. However, had a functor been used, then when moving the validation from one type to another you need only to copy the functor call. The functor implementations stay as is. In this situation a functor is the best solution.
|

