Creative use of extension methods in C #

Hello, Habr!

Continuing our exploration of the C # topic, we have translated for you the following short article regarding the original use of extension methods. We recommend that you pay special attention to the last section concerning interfaces, as well as to profile the author.

I’m sure anyone with even a little bit of C # experience is aware of the existence of extension methods. This is a nice feature that allows developers to extend existing types with new methods.

This is extremely handy in cases where you want to add functionality to types that you do not control. In fact, anyone sooner or later had to write an extension for the BCL, just to make things more accessible.
But, along with relatively obvious use cases, there are also very interesting patterns tied directly to the use of extension methods and demonstrating how they can be used in a less traditional way.

Adding Methods to Enumerations

An enumeration is simply a collection of constant numeric values, each assigned a unique name. Although enumerations in C # inherit from the abstract class Enum, they are not interpreted as real classes. In particular, this limitation prevents them from having methods.

In some cases, it can be helpful to program the logic into an enum. For example, if an enumeration value can exist in several different views and you would like to easily convert one to another.

For example, imagine the following type in a typical application that allows you to save files in various formats:

public enum FileFormat
{
    PlainText,
    OfficeWord,
    Markdown
}

This enumeration defines a list of formats supported in the application and can be used in different parts of the application to initiate branching logic based on a specific value.

Since each file format can be represented as a file extension, it would be nice if each FileFormat contained a method for obtaining this information. It is with the extension method that this can be done, something like this:

public static class FileFormatExtensions
{
    public static string GetFileExtension(this FileFormat self)
    {
        if (self == FileFormat.PlainText)
            return "txt";

        if (self == FileFormat.OfficeWord)
            return "docx";

        if (self == FileFormat.Markdown)
            return "md";

        // Будет выброшено, если мы забудем новый формат файла,
        // но забудем добавить соответствующее расширение файла
        throw new ArgumentOutOfRangeException(nameof(self));
    }
}

Which, in turn, allows us to do this:

var format = FileFormat.Markdown;
var fileExt = format.GetFileExtension(); // "md"
var fileName = $"output.{fileExt}"; // "output.md"

Refactoring Model Classes

It happens that you do not want to add a method directly to the class, for example, if you are working with anemic model

Anemic models are usually represented by a set of public immutable properties, get-only. Therefore, adding methods to a model class may give the impression that the purity of the code is being violated, or you may suspect that the methods refer to some private state. Extension methods do not cause this problem, since they do not have access to the private members of the model and are not by nature part of the model.

So, consider the following example with two models, one representing a closed title list, and the other representing a separate title row:

public class ClosedCaption
{
    // Отображаемый текст
    public string Text { get; }

    // Когда он отображается относительно начала трека 
    public TimeSpan Offset { get; }

    // Как долго текст остается на экране 
    public TimeSpan Duration { get; }

    public ClosedCaption(string text, TimeSpan offset, TimeSpan duration)
    {
        Text = text;
        Offset = offset;
        Duration = duration;
    }
}

public class ClosedCaptionTrack
{
    // Язык, на котором написаны субтитры
    public string Language { get; }

    // Коллекция закрытых надписей
    public IReadOnlyList<ClosedCaption> Captions { get; }

    public ClosedCaptionTrack(string language, IReadOnlyList<ClosedCaption> captions)
    {
        Language = language;
        Captions = captions;
    }
}

In the current state, if we need to get the subtitle string displayed at a particular time, we will run LINQ like this:

var time = TimeSpan.FromSeconds(67); // 1:07

var caption = track.Captions
    .FirstOrDefault(cc => cc.Offset <= time && cc.Offset + cc.Duration >= time);

This really begs some kind of helper method that could be implemented as either a member method or an extension method. I prefer the second option.

public static class ClosedCaptionTrackExtensions
{
    public static ClosedCaption GetByTime(this ClosedCaptionTrack self, TimeSpan time) =>
        self.Captions.FirstOrDefault(cc => cc.Offset <= time && cc.Offset + cc.Duration >= time);
}

In this case, the extension method allows you to achieve the same as the usual one, but gives a number of non-obvious bonuses:

  1. It is clear that this method only works with the public members of the class and does not mysteriously change its private state.
  2. Obviously, this method simply allows you to cut the corner and is provided here for convenience only.
  3. This method belongs to a completely separate class (or even assembly) whose purpose is to separate data from logic.

In general, when using the extension method approach, it is convenient to draw a line between necessary and useful.

How to make interfaces versatile

When designing an interface, you always want the contract to remain minimal, since it will be easier to implement this way. It helps a lot when the interface provides functionality in the most generalized way, so that your colleagues (or yourself) can build on it to handle more specific cases.

If this sounds nonsense to you, consider a typical interface that saves a model to a file:

public interface IExportService
{
    FileInfo SaveToFile(Model model, string filePath);
}

Everything works fine, but in a couple of weeks a new requirement may arrive in time: classes that implement IExportService, should not only export to a file, but also be able to write to a file.

So, to fulfill this requirement, we add a new method to the contract:

public interface IExportService
{
    FileInfo SaveToFile(Model model, string filePath);

    byte[] SaveToMemory(Model model);
}

This change just broke all available implementations IExportServicebecause now they all need to be updated to support writing to memory as well.

But, in order not to do all this, we could have designed the interface a little differently from the very beginning:

public interface IExportService
{
    void Save(Model model, Stream output);
}

In this form, the interface forces you to write the destination in the most generalized form, that is, this Stream… Now we are no longer limited to files when working and can also target various other output options.

The only drawback of this approach is that the most basic operations are not as simple as we are used to: now we have to set a specific instance Stream, wrap it in a using statement and pass it as a parameter.

Fortunately, this drawback is completely nullified when using extension methods:

public static class ExportServiceExtensions
{
    public static FileInfo SaveToFile(this IExportService self, Model model, string filePath)
    {
        using (var output = File.Create(filePath))
        {
            self.Save(model, output);
            return new FileInfo(filePath);
        }
    }

    public static byte[] SaveToMemory(this IExportService self, Model model)
    {
        using (var output = new MemoryStream())
        {
            self.Save(model, output);
            return output.ToArray();
        }
    }
}

By refactoring the original interface, we made it much more versatile and didn’t sacrifice usability by using extension methods.

Thus, I find extension methods an invaluable tool that allows keep simple simple and turn complex into possible

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *