Home > Articles > Programming > C#

More Effective C#: Item 29: Enhance Constructed Types with Extension Methods

  • Print
  • + Share This
  • 💬 Discuss
Bill Wagner shows how you can create a set of extension methods on specific constructed types to implement that functionality in a very low impact way.

You’re likely going to use a number of constructed generic types in your application. You’ll create specific collection types: List<int>, Dictionary<EmployeeID, Employee> and many other collections. The purpose for these different collections is that your application has some specific need for a collection of a certain types and will want to have specific behavior defined for those specific constructed types. You can create a set of extension methods on specific constructed types to implement that functionality in a very low impact way.

You can see this pattern in the System.Linq.Enumerable class. Item 26 discussed the extension pattern used by Enumerable<T> to implement many common methods on sequences as extension methods on IEnumerable<T>. In addition, Enumerable contains a number of methods that are implemented specifically for particular constructed types that implement IEnumerable<T>. For instance, several numeric methods are implemented on numeric sequences (IEnumerable<int>, IEnumerable<double>, IEnumerable<long> and IEnumerable<float>). Here are a few of the extension methods implemented specifically for IEnumerable<int>:

public class Enumerable
{
    public static int Average(this IEnumerable<int> sequence);
    public static int Max(this IEnumerable<int> sequence);
    public static int Min(this IEnumerable<int> sequence);
    public static int Sum(this IEnumerable<int> sequence);

    // other methods elided
}

Once you recognize the pattern, you can see many ways you could implement the same type of extensions for the constructed types in your own domain. If you were writing an eCommerce application and you wanted to send email coupons to a set of customers, the method signature might look something like this:

public static void SendEmailCoupons(this IEnumerable<Customer>

    customers, Coupon specialOffer);

Similarly, you could find all customers with no orders in the last month:

public static IEnumerable<Customer> LostProspects(
    this IEnumerable<Customer> targetList);

If you didn’t have extension methods, you could achieve a similar affect by deriving a new type from the constructed generic type you used. For instance, the Customer methods I just showed could be implemented like this:

public class CustomerList : List<Customer>
{
    public void SendEmailCoupons(Coupon specialOffer);
    public static IEnumerable<Customer> LostProspects();
   
}

It works, but it is actually much more limiting to the users of this List of customers. The difference in the method signatures provides part of the reason. The extension methods use IEnumerable<Customer> as the parameter, but the methods added to the derived class are based on List<Customer>. They mandate a particular storage model. Because they mandate a particular storage model, they can’t be composed as a set of iterator methods (See Item 17). You’ve placed unnecessary design constraints on the users of these methods. That’s a misuse of inheritance.

Another reason to prefer the extension methods as a way to implement this functionality has to do with how queries are composed. The LostProspects() method probably would be implemented something like this:

public static IEnumerable<Customer> LostProspects(
    IEnumerable<Customer> targetList)
{
    IEnumerable<Customer> answer =
        from c in targetList
        where DateTime.Now - c.LastOrderDate > TimeSpan.FromDays(30)
        select c;
    return answer;
}

Item 35 discusses why lambda expressions are preferred over methods in queries. Implementing these features as extension methods means that they provide a reusable query expressed as a lambda expression. You can reuse the entire query, rather than trying to reuse the predicate of the where clause.

If you examine the object model for any application or library you are writing, you’ll likely find many constructed types used for the storage model. You should look at these constructed types and decide what methods logically would be added to each of those constructed types. Create the implementation for those methods as extension methods using the constructed type, or a constructed interface implemented by the type. You’ll turn a simple generic instantiation into a class with all the behavior you need. Furthermore, you’ll create that implementation in a manner that decouples the storage model from the implementation to the greatest extent possible.

  • + Share This
  • 🔖 Save To Your Account

Discussions

comments powered by Disqus