web-dev-qa-db-fra.com

Quand écrire des méthodes d'extension pour vos propres classes?

J'ai récemment vu une base de code qui avait une classe de données Address définie quelque part puis à un autre endroit:

fun Address.toAnschrift() = let { address ->
    Anschrift().apply {
        // mapping code here...
    }
}

J'ai trouvé confus de ne pas avoir cette méthode directement sur l'adresse. Existe-t-il des modèles établis ou des meilleures pratiques quant à l'utilisation des méthodes d'extension? Ou le livre à ce sujet doit-il encore être écrit?

Notez que malgré la syntaxe Kotlin dans mon exemple, je suis intéressé par les meilleures pratiques générales car cela s'appliquerait également à C # ou à d'autres langages avec ces capacités.

8
Robert Jack Will

Les méthodes d'extension ne fournissent pas quelque chose qui ne peut pas être fait autrement. Ce sont des sucres syntaxiques pour rendre le développement plus agréable, sans apporter d'avantages techniques réels.


Les méthodes d'extension sont relativement nouvelles. Les normes et les conventions sont généralement décidées par l'opinion populaire (plus une expérience à long terme sur ce qui finit par être une mauvaise idée), et je ne pense pas que nous soyons au point où nous pouvons tracer une ligne définitive avec laquelle tout le monde est d'accord.

Cependant, je peux voir plusieurs arguments, basés sur des choses que j'ai entendues de collègues.

1. Ils remplacent généralement les méthodes statiques.

Les méthodes d'extension sont le plus souvent basées sur des choses qui auraient autrement été des méthodes statiques, pas des méthodes de classe. Elles ressemblent aux méthodes de classe , mais elles ne le sont pas vraiment.

Les méthodes d'extension ne doivent tout simplement pas être considérées comme une alternative aux méthodes de classe; mais plutôt comme un moyen de donner aux méthodes autrement ugle statiques une approche "classe méthodique".

2. Lorsque votre méthode prévue est spécifique à un projet consommateur mais pas à la bibliothèque source.

Ce n'est pas parce que vous développez à la fois la bibliothèque et le consommateur que vous êtes prêt à mettre la logique là où elle convient.

Par exemple:

  • Vous avez une classe PersonDto provenant de votre projet DataLayer.
  • Votre projet WebUI souhaite pouvoir convertir un PersonDto en PersonViewModel.

Vous ne pouvez pas (et ne voudriez pas) ajouter cette méthode de conversion à votre projet DataLayer. Étant donné que cette méthode est étendue au projet WebUI, elle doit vivre à l'intérieur de ce projet.

Une méthode d'extension vous permet d'avoir cette méthode accessible globalement dans votre projet (et les consommateurs potentiels de votre projet), sans nécessiter la bibliothèque DataLayer pour l'implémenter.

. Si vous pensez que les classes de données ne doivent contenir que des données.

Par exemple, votre classe Person contient les propriétés d'une personne. Mais vous voulez pouvoir disposer de quelques méthodes pour formater certaines des données:

public class Person
{
    //The properties...

    public SecurityQuestionAnswers GetSecurityAnswers()
    {
        return new SecurityQuestionAnswers()
        {
            MothersMaidenName = this.Mother.MaidenName,
            FirstPetsName = this.Pets.OrderbyDescending(x => x.Date).FirstOrDefault()?.Name
        };
    }
}

J'ai vu beaucoup de collègues qui détestent l'idée de mélanger les données et la logique dans une classe. Je ne suis pas tout à fait d'accord avec des choses simples comme le formatage des données, mais je concède que dans l'exemple ci-dessus, il semble sale pour Person de devoir dépendre de SecurityQuestionAnswers.

Placer cette méthode dans une méthode d'extension empêche d'entacher la classe de données par ailleurs pure.

Notez que cet argument est de style. Ceci est similaire aux classes partielles. Il n'y a pas ou peu d'avantages techniques à le faire, mais cela vous permet de séparer le code en plusieurs fichiers si vous le jugez plus propre (par exemple, si de nombreuses personnes utilisent la classe mais ne se soucient pas de vos méthodes personnalisées supplémentaires).

4. Méthodes d'assistance

Dans la plupart des projets, j'ai tendance à me retrouver avec des cours d'aide. Ce sont des classes statiques qui fournissent généralement une collection de méthodes pour un formatage facile.

Par exemple. une entreprise dans laquelle je travaillais avait un format datetime particulier qu'elle voulait utiliser. Plutôt que d'avoir à coller la chaîne de format partout, ou de faire de la chaîne de format une variable globale, j'ai opté pour une méthode d'extension DateTime:

public static string ToCompanyFormat(this DateTime dt)
{
    return dt.ToString("...");
} 

Est-ce mieux qu'une méthode d'assistance statique normale? Je le pense. Il nettoie la syntaxe. Au lieu de

DateTimeHelper.ToCompanyFormat(myObj.CreatedOn);

Je pourrais faire:

myObj.CreatedOn.ToCompanyFormat();

Ceci est similaire à une version courante de la même syntaxe. Je l'aime plus, même s'il n'y a aucun avantage technique à avoir l'un sur l'autre.


Tous ces arguments sont subjectifs. Aucun d'entre eux ne couvre un cas qui, autrement, ne pourrait jamais être couvert.

  • Chaque méthode d'extension peut facilement être réécrite en une méthode statique normale.
  • Le code peut être séparé dans des fichiers à l'aide de classes partielles, même si les méthodes d'extension n'existaient pas.

Mais là encore, nous pouvons prendre votre affirmation selon laquelle elles ne sont jamais nécessaires à l'extrême:

  • Pourquoi avons-nous besoin de méthodes de classe, si nous pouvons toujours créer des méthodes statiques avec un nom qui indique clairement que c'est pour une classe particulière?

La réponse à cette question et à la vôtre reste la même:

  • Syntaxe plus agréable
  • Meilleure lisibilité et moins de pédanterie de code (le code va arriver au point en moins de caractères)
  • Intellisense fournit des résultats contextuellement significatifs (les méthodes d'extension ne sont affichées que sur le type correct. Les classes statiques sont utilisables partout).

Une mention supplémentaire particulière cependant:

  • Les méthodes d'extension permettent un style de codage de chaîne de méthode; qui est devenu plus populaire ces derniers temps, par opposition à la syntaxe plus enveloppante de la méthode Vanilla. Des préférences syntaxiques similaires peuvent être vues dans la syntaxe des méthodes de LINQ.
  • Alors que le chaînage de méthodes peut être fait en utilisant également des méthodes de classe; gardez à l'esprit que cela ne s'applique pas tout à fait aux méthodes statiques. Les méthodes d'extension sont le plus souvent basées sur des choses qui auraient autrement été des méthodes statiques, pas des méthodes de classe.
15
Flater

Je suis d'accord avec Flater qu'il n'y a pas eu assez de temps pour que la convention se cristallise, mais nous avons l'exemple des bibliothèques de classes .NET Framework elles-mêmes. Considérez que lorsque des méthodes LINQ ont été ajoutées à .NET, elles n'ont pas été ajoutées directement sur IEnumerable, mais en tant que méthodes d'extension.

Que pouvons-nous en tirer? LINQ est

  1. un ensemble cohérent de fonctionnalités qui n'est pas toujours nécessaire,
  2. est destiné à être utilisé dans le style de chaînage de méthodes (par exemple A.B().C() au lieu de C(B(A))), et
  3. ne nécessite pas l'accès à l'état privé de l'objet

Si vous disposez de ces trois fonctionnalités, les méthodes d'extension ont beaucoup de sens. En fait, je dirais que si un ensemble de méthodes a la fonctionnalité n ° 3 et l'une des deux premières fonctionnalités, vous devriez envisager d'en faire des méthodes d'extension.

Méthodes d'extension:

  1. Vous permettent d'éviter de polluer l'API de base d'une classe,
  2. Non seulement autorise le style de chaînage de méthodes, mais lui permet de gérer de manière plus flexible les valeurs de null car une méthode d'extension appelée à partir d'une valeur de null reçoit simplement le null comme premier argument,
  3. Vous empêcher d'accéder/de modifier accidentellement l'état privé/protégé dans une méthode qui ne devrait pas y avoir accès

Les méthodes d'extension ont un autre avantage: elles peuvent toujours être utilisées comme méthode statique et peuvent être transmises directement en tant que valeur à une fonction d'ordre supérieur. Donc, si MyMethod est une méthode d'extension, au lieu de dire myList.Select(x => x.MyMethod()), vous pouvez simplement utiliser myList.Select(MyMethod). Je trouve ça plus agréable.

5
James Jensen

La force motrice derrière les méthodes d'extension est la possibilité d'ajouter des fonctionnalités dont vous avez besoin à toutes les classes ou interfaces d'un certain type, qu'elles soient scellées ou dans un autre assembly. Vous pouvez en voir la preuve avec le code LINQ, en ajoutant des fonctions à tous les IEnumerable<T> objets, qu'il s'agisse de listes ou de piles, etc. Le concept était de pérenniser les fonctions de sorte que si vous aviez votre propre IEnumerable<T> vous pouvez utiliser automatiquement LINQ avec.

Cela dit, la fonction de langue est suffisamment nouvelle pour que vous n'ayez aucune directive sur le moment de les utiliser ou de ne pas les utiliser. Cependant, ils permettent un certain nombre de choses auparavant impossibles:

  • API fluides (voir Assertions fluides pour un exemple de comment ajouter des API fluides à n'importe quel objet)
  • Intégration des fonctionnalités (NetCore Asp.NET MVC utilise des méthodes d'extension pour câbler les dépendances et les fonctionnalités dans le code de démarrage)
  • Localiser l'impact d'une fonctionnalité (votre exemple dans la déclaration d'ouverture)

Pour être juste, seul le temps dira dans quelle mesure est trop loin, ou à quel point les méthodes d'extension deviennent un obstacle plutôt qu'une aubaine. Comme mentionné dans une autre réponse, il n'y a rien dans une méthode d'extension qui ne peut pas être fait avec une méthode statique. Cependant, lorsqu'ils sont utilisés judicieusement, ils peuvent rendre le code plus facile à lire.

Qu'en est-il de l'utilisation intentionnelle de méthodes d'extensions dans le même assembly?

Je pense qu'il est utile d'avoir des fonctionnalités de base bien testées et fiables, et de ne pas jouer avec jusqu'à ce que vous deviez absolument le faire. Nouveau code qui en dépend, mais qui en a besoin pour fournir des fonctionnalités uniquement au profit du nouveau code peut utiliser des méthodes d'extension pour ajouter des fonctionnalités pour prendre en charge le nouveau code. Ces fonctions n'ont d'intérêt que pour le nouveau code, et la portée des méthodes d'extension est limitée à l'espace de noms dans lequel elles sont déclarées.

Je n'écarterais pas aveuglément l'utilisation de méthodes d'extension, mais je considérerais si la nouvelle fonctionnalité devrait vraiment être ajoutée au code de base existant qui est bien testé.

4
Berin Loritsch

Existe-t-il des modèles établis ou des meilleures pratiques quant à l'utilisation des méthodes d'extension?

L'une des raisons les plus courantes d'utiliser des méthodes d'extension est comme un moyen "d'étendre, pas de modifier" les interfaces. Plutôt que d'avoir IFoo et IFoo2, où ce dernier introduit une nouvelle méthode, Bar, Bar est ajouté à IFoo via une méthode d'extension.

L'une des raisons pour lesquelles Linq utilise des méthodes d'extension pour Where, Select etc est afin de permettre aux utilisateurs de définir leurs propres versions de ces méthodes, qui sont ensuite également utilisées par la syntaxe de requête Linq.

Au-delà de cela, l'approche que j'utilise, qui semble correspondre à ce que je vois couramment dans le framework .NET au moins, est:

  1. Mettez les méthodes directement liées à la classe dans la classe elle-même,
  2. Mettez toutes les méthodes qui ne sont pas liées à la fonctionnalité de base de la classe dans les méthodes d'extension.

Donc, si j'ai un Option<T>, Je mettrais des choses comme HasValue, Value, ==, etc. dans le type lui-même. Mais quelque chose comme une fonction Map, qui convertit la monade de T1 à T2, Je mettrais une méthode d'extension:

public static Option<TOutput> Map<TInput, TOutput>(this Option<TInput> input, 
                                                   Func<TInput, TOutput> f) => ...
1
David Arno