C# – Replacing LINQ Methods with Extension Methods

cextension-methodlinqobject-orientedobject-oriented-design

So, I've fallen into the fad trap, and started replacing a large amount of linq queries with extension methods.

For example:

orders.Where(o => o.Status == ShippedStatus.Shipped).Select(o => o.ID)

has become :

orders.ShippedOrderIds

Extension methods are static with all the cons implied, and I believe the 'most-correct' OO way to handle this kind of refactor would be to wrap it in a 'Orders' object and expose property(ies) that do this instead.

A couple questions:

  1. Is there a third (or more) alternative that makes more sense than either of these approaches?
  2. How much worse is the extension approach than the 'true' OO approach?

Quick clarification of the refactoring contexts – none of these refactors operate on single objects, just collections of objects.

Best Answer

Extension methods offer you an opportunity to reason about problems a totally different way-- functionally. Functional programming is a totally different paradigm from object-oriented and has certain advantages. For example, when you use functional thinking and immutable variables, your program instantly becomes thread-safe without using any locks or concurrency mechanisms at all. In addition, when you write pure functions, unit testing is a snap, and you don't need any mocks or stubs at all. What's more, extension methods allow you to put methods on interfaces-- no class required at all-- and you only need to implement them once and they will operate on any class that implements the interface, even if they don't share a base class.

Here are the rules to make your code work well in this fashion:

  1. Write your extension methods so that they are pure. They should not access any state outside of the method inputs and should not modify any state at all.

  2. Write your extension methods so they always return a new instance, rather than modifying the instance that was handed to it.

  3. Write them generically when possible; this allows type-safe method chaining that can transform types.

  4. If you need the methods to have dependencies, inject them as parameters, e.g. Where accepts a delegate which allows the caller to "inject" a logic engine (exposed as a Func<T,bool>) that determines whether a row will be included.

By the way, all LINQ methods conform to these rules.

So for example, if you have several types of classes that have a "shipped" status, you could implement this:

interface IShippable
{
    ShippedStatus ShippedStatus { get; }
    int Id;
}


static public IEnumerable<T> ShippedOrderIds<T>(this IEnumerable<T> source) where T : IShippable
{
    return source.Where( s => s.ShippedStatus == ShippedState.Shipped ).Select( s => s.Id );
}

This will work with any shippable item without requiring any upcasting or downcasting; it is pure, so you don't need to inject any dependencies and there is nothing to mock or isolate; and it will allow type-safe method chaining if you write other extension methods.

This is not a bad way to go at all. The only way it could be bad is if you think you are writing object-oriented code. You're not. You're writing functional code, and that can be a good thing.