Uso creativo de métodos de extensión en C #

¡Hola, Habr!



Continuando con nuestra exploración del tema de C #, le hemos traducido el siguiente artículo breve sobre el uso original de los métodos de extensión. Te recomendamos que prestes especial atención al último apartado relativo a las interfaces, así como al perfil del autor.







Estoy seguro de que cualquier persona con un poco de experiencia en C # es consciente de la existencia de métodos de extensión. Esta es una característica interesante que permite a los desarrolladores ampliar los tipos existentes con nuevos métodos.



Esto es extremadamente útil en los casos en los que desea agregar funcionalidad a tipos que no controla. De hecho, cualquiera, tarde o temprano, tuvo que escribir una extensión para la BCL solo para hacer las cosas más accesibles.



Pero, junto con casos de uso relativamente obvios, también hay patrones muy interesantes vinculados directamente al uso de métodos de extensión y que demuestran cómo se pueden usar de una manera menos tradicional.



Agregar métodos a las enumeraciones



Una enumeración es simplemente una colección de valores numéricos constantes, a cada uno se le asigna un nombre único. Aunque las enumeraciones en C # heredan de la clase abstracta Enum, no se interpretan como clases reales. En particular, esta limitación les impide tener métodos.



En algunos casos, puede resultar útil programar la lógica en una enumeración. Por ejemplo, si un valor de enumeración puede existir en varias vistas diferentes y le gustaría convertir fácilmente una en otra.



Por ejemplo, imagine el siguiente tipo en una aplicación típica que le permite guardar archivos en varios formatos:



public enum FileFormat
{
    PlainText,
    OfficeWord,
    Markdown
}


Esta enumeración define una lista de formatos admitidos en la aplicación y se puede utilizar en diferentes partes de la aplicación para iniciar la lógica de ramificación basada en un valor específico.



Dado que cada formato de archivo se puede representar como una extensión de archivo, sería bueno si cada uno FileFormattuviera un método para obtener esta información. Es con el método de extensión que se puede hacer esto, algo como esto:



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));
    }
}


Lo que, a su vez, nos permite hacer esto:



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


Refactorización de clases de modelos



Hay ocasiones en las que no desea agregar un método directamente a una clase, por ejemplo, si está trabajando con un modelo anémico .



Los modelos anémicos suelen estar representados por un conjunto de propiedades públicas inmutables, solo de obtención. Por lo tanto, al agregar métodos a una clase modelo, puede tener la impresión de que se viola la pureza del código o puede sospechar que los métodos se refieren a algún tipo de estado privado. Los métodos de extensión no causan este problema, ya que no tienen acceso a los miembros privados del modelo y, por naturaleza, no forman parte del modelo.



Por lo tanto, considere el siguiente ejemplo con dos modelos, uno que representa una lista de títulos cerrada y el otro que representa una fila de título separada:



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;
    }
}


En el estado actual, si necesitamos que se muestre la cadena de subtítulos en un momento en particular, ejecutaremos LINQ así:



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

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


De hecho, esto requiere algún tipo de método auxiliar que pueda implementarse como método miembro o método de extensión. Prefiero la segunda opción.



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);
}


En este caso, el método de extensión le permite lograr lo mismo que el habitual, pero ofrece una serie de bonificaciones no obvias:



  1. Está claro que este método solo funciona con miembros públicos de la clase y no cambia su estado privado de alguna manera misteriosa.
  2. Obviamente, este método simplemente le permite cortar la esquina y se proporciona aquí solo por conveniencia.
  3. Este método pertenece a una clase completamente separada (o incluso ensamblado) cuyo propósito es separar los datos de la lógica.


En general, cuando se utiliza el enfoque del método de extensión, es conveniente trazar una línea entre lo necesario y lo útil.



Hacer que las interfaces sean versátiles



Al diseñar una interfaz, siempre desea que el contrato se mantenga lo más pequeño posible porque facilita su implementación. Ayuda mucho cuando la interfaz proporciona la funcionalidad de la manera más generalizada, de modo que sus colegas (o usted mismo) puedan aprovecharla para manejar casos más específicos.



Si esto le parece una tontería, considere una interfaz típica que guarda un modelo en un archivo:



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


Todo funciona bien, pero en un par de semanas puede llegar un nuevo requisito: las clases que implementen IExportServiceno solo deben exportar a un archivo, sino también poder escribir en un archivo.



Entonces, para cumplir con este requisito, agregamos un nuevo método al contrato:



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

    byte[] SaveToMemory(Model model);
}


Este cambio acaba de romper todas las implementaciones existentes, IExportServiceya que ahora todas deben actualizarse para admitir también la escritura en la memoria.



Pero, para no hacer todo esto, podríamos haber diseñado la interfaz de manera un poco diferente desde el principio:



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


De esta forma, la interfaz te obliga a escribir el destino en la forma más generalizada, es decir, esto Stream. Ahora ya no estamos limitados a los archivos cuando trabajamos y también podemos apuntar a otras opciones de salida.



El único inconveniente de este enfoque es que las operaciones más básicas no son tan simples como estamos acostumbrados: ahora tienes que establecer una instancia específica Stream, envolverla en una instrucción using y pasarla como parámetro.



Afortunadamente, este inconveniente se anula por completo cuando se utilizan métodos de extensión:



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();
        }
    }
}


Al refactorizar la interfaz original, la hicimos mucho más versátil y no sacrificamos la usabilidad mediante el uso de métodos de extensión.



Por lo tanto, considero que los métodos de extensión son una herramienta invaluable para mantener lo simple simple y convertir lo complejo en posible .



All Articles