Part 1 presented a recap of extension methods. If you're not familiar with the concept, you should check it first.
Cohesion
The main concern with extension methods is that they aren't for everything; they have to be cohesive with the type being extended.
Let's say we add another extension method to the string class from the previous post:
public static class StringExtensions
{
public static bool IsValidEmailAddress(this string instance)
{
if (instance.HasValue())
{
Regex expression = new Regex(
@"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'
*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z
0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?");
return expression.IsMatch(instance);
}
else
{
return false;
}
}
}
Does this really make sense? Definitely not!
A string is not intended to contain "exclusively" email addresses; so you end up polluting your string class with domain-specific extensions.
public class StringExtensionsFixture
{
[Fact]
public void ConnectionString_Polluted()
{
string connectionString = "Data Source=myServerAddress;Initial Catalog=myDataBase;Integrated Security=SSPI;";
Assert.False(connectionString.IsValidEmailAddress()); //Doesn't make sense!
}
}
Facade
Extension Methods should really be used to facade and simplify usage of existing scenarios without adding extra coupling on external concepts:
public static class TypeExtensions
{
public static bool Is<T>(this Type type)
{
return typeof(T).IsAssignableFrom(type);
}
}
public static class TypeExtensionsFixture
{
[Fact]
public void Is()
{
// Without the extension, logic relies on the understanding of the obscure method "IsAssignableFrom".
Assert.True(typeof(IComparable).IsAssignableFrom(typeof(int)));
// With the extension, the developer leverages existing knowledge of the "is" operator
Assert.True(typeof(int).Is<IComparable>());
}
}
One imporant thing to notice here is the usage of a "generic" extension method. We'll delve into more details about generic extension methods in a subsequent post.
However, some cross-cutting concerns such as Serialization, Conversion, Equality benefit largely from being published as extension methods instead of remaining in their own "helper" classes. The main reason for this is discoverability. Some people will argue that since the extension method isn't part of the extended type per se, it lowers discoverability. "Helper" methods aren't any more discoverable.
Extension Point
Let's say we want to add the serialization "functionality" to any object, we could use extension methods and write something like:
public static class SerializationExtension
{
public static void ToXml<T>(this T instance, string fileName)
{
XmlSerializer serializer = new XmlSerializer(typeof(T));
FileStream stream = File.OpenWrite(fileName);
using (stream)
{
serializer.Serialize(stream, instance);
}
}
public static void ToBinary<T>(this T instance, string fileName)
{
BinaryFormatter formatter = new BinaryFormatter();
FileStream stream = File.OpenWrite(fileName);
using (stream)
{
formatter.Serialize(stream, instance);
}
}
}
What's the problem here? We started polluting the "Person" type with non-related concerns such as Xml and Binary serialization. Just imagine one moment all the additional methods that could suddenly appear and pollute the Person object. It would lead to "Extension Method Hell".
One remedy is to try to group those methods into "Extension Points", grouping them by how related the concern is.
Introducing: the Extension Point object:
public class SerializationExtensionPoint<T>
{
public T Value { get; set; }
}
The goal of the extension point is to wrap an instance with a "typed scope", allowing the extension methods to be programmed against the extension point instead of the instance itself.
public static class SerializationExtension
{
public static void ToXml<T>(this T instance, string fileName)
{
XmlSerializer serializer = new XmlSerializer(typeof(T));
FileStream stream = File.OpenWrite(fileName);
using (stream)
{
serializer.Serialize(stream, instance);
}
}
public static void ToBinary<T>(this T instance, string fileName)
{
BinaryFormatter formatter = new BinaryFormatter();
FileStream stream = File.OpenWrite(fileName);
using (stream)
{
formatter.Serialize(stream, instance);
}
}
}
We've added the Serialization() extension method that provides the typed scope. Other scopes could group non-functional extensions like Equality, Conversion and Comparison.
In the image above, we can clearly see that the interface is a lot more cohesive and has only been polluted with a Serialization aspect.
The complete code would look something like:
public class SerializationExtensionFixture
{
[Fact]
public void ToXml()
{
Person person = new Person { FirstName = "John", LastName = "Doe" };
person.Serialization().ToXml("john.xml");
}
}
In the next post, we'll try to explore in more details how we can benefit from generics in extension methods.