Utilisation créative des méthodes d'extension en C #

Bonjour, Habr!



Poursuivant notre exploration du sujet C #, nous avons traduit pour vous le court article suivant concernant l'utilisation originale des méthodes d'extension. Nous vous recommandons de porter une attention particulière à la dernière section concernant les interfaces, ainsi qu'au profil de l' auteur.







Je suis sûr que toute personne ayant même un peu d'expérience C # est consciente de l'existence de méthodes d'extension. C'est une fonctionnalité intéressante qui permet aux développeurs d'étendre les types existants avec de nouvelles méthodes.



Ceci est extrêmement pratique dans les cas où vous souhaitez ajouter des fonctionnalités à des types que vous ne contrôlez pas. En fait, n'importe qui, tôt ou tard, a dû écrire une extension pour la BCL juste pour rendre les choses plus accessibles.



Mais, avec des cas d'utilisation relativement évidents, il existe également des modèles très intéressants liés directement à l'utilisation de méthodes d'extension et montrent comment ils peuvent être utilisés de manière non traditionnelle.



Ajout de méthodes aux énumérations



Une énumération est simplement une collection de valeurs numériques constantes, chacune affectée d'un nom unique. Bien que les énumérations en C # héritent de la classe abstraite Enum, elles ne sont pas interprétées comme des classes réelles. En particulier, cette limitation les empêche de disposer de méthodes.



Dans certains cas, il peut être utile de programmer la logique dans une énumération. Par exemple, si une valeur d'énumération peut exister dans plusieurs vues différentes et que vous souhaitez facilement la convertir l'une en une autre.



Par exemple, imaginez le type suivant dans une application typique qui vous permet d'enregistrer des fichiers dans différents formats:



public enum FileFormat
{
    PlainText,
    OfficeWord,
    Markdown
}


Cette énumération définit une liste de formats pris en charge par l'application et peut être utilisée dans différentes parties de l'application pour lancer une logique de branchement basée sur une valeur spécifique.



Étant donné que chaque format de fichier peut être représenté comme une extension de fichier, ce serait bien si chacun FileFormatavait une méthode pour obtenir ces informations. C'est avec la méthode d'extension que cela peut être fait, quelque chose comme ceci:



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


Ce qui, à son tour, nous permet de faire ceci:



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


Refactorisation des classes de modèle



Il y a des moments où vous ne souhaitez pas ajouter une méthode directement à une classe, par exemple, si vous travaillez avec un modèle anémique .



Les modèles anémiques sont généralement représentés par un ensemble de propriétés publiques immuables, en lecture seule. Par conséquent, lors de l'ajout de méthodes à une classe de modèle, vous pouvez avoir l'impression que la pureté du code est violée, ou vous pouvez suspecter que les méthodes font référence à une sorte d'état privé. Les méthodes d'extension ne posent pas ce problème, car elles n'ont pas accès aux membres privés du modèle et ne font pas par nature partie du modèle.



Prenons l'exemple suivant avec deux modèles, l'un représentant une liste de titres fermée et l'autre représentant une ligne de titre distincte:



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


Dans l'état actuel, si nous devons afficher la chaîne de sous-titres à un moment donné, nous exécuterons LINQ comme ceci:



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

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


Cela demande vraiment une sorte de méthode d'assistance qui pourrait être implémentée en tant que méthode membre ou méthode d'extension. Je préfère la deuxième 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);
}


Dans ce cas, la méthode d'extension vous permet d'obtenir le même résultat que d'habitude, mais donne un certain nombre de bonus non évidents:



  1. Il est clair que cette méthode ne fonctionne qu'avec les membres publics de la classe et ne change pas son état privé d'une manière mystérieuse.
  2. De toute évidence, cette méthode vous permet simplement de couper le coin et n'est fournie ici que par commodité.
  3. Cette méthode appartient à une classe (voire à un assembly) complètement distincte dont le but est de séparer les données de la logique.


En général, lorsque vous utilisez l'approche de la méthode d'extension, il est pratique de tracer une ligne entre nécessaire et utile.



Rendre les interfaces polyvalentes



Lors de la conception d'une interface, vous voulez toujours que le contrat soit aussi petit que possible car il le rend plus facile à mettre en œuvre. Cela aide beaucoup lorsque l'interface fournit des fonctionnalités de la manière la plus généralisée, afin que vos collègues (ou vous-même) puissent s'appuyer dessus pour gérer des cas plus spécifiques.



Si cela vous semble insensé, envisagez une interface typique qui enregistre un modèle dans un fichier:



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


Tout fonctionne bien, mais dans quelques semaines, une nouvelle exigence peut arriver: les classes qui implémentent IExportServicedoivent non seulement exporter vers un fichier, mais aussi pouvoir écrire dans un fichier.



Ainsi, pour répondre à cette exigence, nous ajoutons une nouvelle méthode au contrat:



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

    byte[] SaveToMemory(Model model);
}


Ce changement vient de casser toutes les implémentations existantes IExportServicecar elles doivent maintenant toutes être mises à jour pour prendre en charge l'écriture en mémoire également.



Mais pour ne pas faire tout cela, nous aurions pu concevoir l'interface un peu différemment dès le début:



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


Sous cette forme, l'interface vous oblige à écrire la destination sous la forme la plus généralisée, c'est-à-dire ceci Stream. Désormais, nous ne sommes plus limités aux fichiers lorsque nous travaillons et pouvons également cibler diverses autres options de sortie.



Le seul inconvénient de cette approche est que les opérations les plus élémentaires ne sont pas aussi simples que ce à quoi nous sommes habitués: nous devons maintenant définir une instance spécifique Stream, l'envelopper dans une instruction using et la passer en paramètre.



Heureusement, cet inconvénient est complètement annulé lors de l'utilisation de méthodes d'extension:



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


En refactorisant l'interface d'origine, nous l'avons rendue beaucoup plus polyvalente et n'avons pas sacrifié la convivialité en utilisant des méthodes d'extension.



En tant que telles, je trouve que les méthodes d'extension sont un outil précieux pour garder le simple simple et transformer le complexe en possible .



All Articles