Modèle C # pour gérer proprement les «fonctions libres», en évitant les classes statiques de «sac utilitaire» de type Helper

9

Je passais récemment en revue quelques classes statiques de "sac utilitaire" de style Helper flottant autour de grandes bases de code C # avec lesquelles je travaille, des choses comme l'extrait de code très condensé suivant:

// Helpers.cs

public static class Helpers
{
    public static void DoSomething() {}
    public static void DoSomethingElse() {}
}

Les méthodes spécifiques que j'ai examinées sont

  • la plupart du temps sans rapport les uns avec les autres,
  • sans état explicite persistait à travers les invocations,
  • petit, et
  • chacun consommé par une variété de types non liés.

Edit: Ce qui précède n'est pas destiné à être une liste de problèmes allégués. Il s'agit d'une liste des caractéristiques communes des méthodes spécifiques que j'examine. C'est le contexte pour aider les réponses à fournir des solutions plus pertinentes.

Juste pour cette question, je ferai référence à ce type de méthode comme un GLUM (méthode générale de l'utilitaire léger). La connotation négative de "morosité" est en partie voulue. Je suis désolé si cela apparaît comme un jeu de mots stupide.

Même en mettant de côté mon propre scepticisme par défaut à l'égard des GLUM, je n'aime pas les choses suivantes à ce sujet:

  • Une classe statique est utilisée uniquement comme espace de noms.
  • L'identifiant de classe statique n'a pratiquement aucun sens.
  • Lorsqu'un nouveau GLUM est ajouté, soit (a) cette classe "bag" est touchée sans raison valable, soit (b) une nouvelle classe "bag" est créée (ce qui en soi n'est généralement pas un problème; ce qui est mauvais, c'est que le nouveau les classes statiques ne font que répéter le problème de l'absence de lien, mais avec moins de méthodes).
  • La méta-nommage est inéluctablement terrible, non standard, et généralement dépourvue de cohérence interne, que ce soit Helpers, Utilitiesou autre chose.

Quel est un modèle raisonnablement bon et simple pour refactoriser cela, de préférence en répondant aux préoccupations ci-dessus, et de préférence avec un toucher aussi léger que possible?

Je devrais probablement insister sur le fait que toutes les méthodes auxquelles je fais face ne sont pas liées les unes aux autres. Il ne semble pas y avoir de moyen raisonnable de les décomposer en sacs de méthode de classe statique plus fins mais toujours multi-membres.

William
la source
1
À propos de GLUMs ™: Je pense que l'abréviation de "léger" pourrait être supprimée, car les méthodes devraient être légères en suivant les meilleures pratiques;)
Fabio
@Fabio, je suis d'accord. J'ai inclus le terme comme une blague (probablement pas très drôle et peut-être déroutante) qui sert d'abréviation à la rédaction de cette question. Je pense que "léger" semblait approprié ici parce que les bases de code en question sont de longue durée, héritées et un peu désordonnées, c'est-à-dire qu'il existe de nombreuses autres méthodes (pas habituellement ces GUM ;-) qui sont "lourdes". Si j'avais plus de budget (ehem, any) pour le moment, je prendrais certainement une approche plus agressive et m'occuperais de ce dernier.
William

Réponses:

17

Je pense que vous avez deux problèmes étroitement liés.

  • Les classes statiques contiennent des fonctions logiquement indépendantes
  • Les noms des classes statiques ne fournissent pas d'informations utiles sur la signification / le contexte des fonctions contenues.

Ces problèmes peuvent être résolus en combinant uniquement des méthodes logiquement liées dans une classe statique et en déplaçant les autres dans leur propre classe statique. En fonction de votre domaine problématique, vous et votre équipe devez décider des critères que vous utiliserez pour séparer les méthodes en classes statiques associées.

Préoccupations et solutions possibles

Les méthodes ne sont pour la plupart pas liées les unes aux autres - problème. Combinez les méthodes liées sous une classe statique et déplacez les méthodes non liées vers leurs propres classes statiques.

Les méthodes sont sans état explicite persistées à travers les invocations - ce n'est pas un problème. Les méthodes sans état sont une fonctionnalité, pas un bogue. L'état statique est difficile à tester de toute façon et ne doit être utilisé que si vous devez maintenir l'état à travers les appels de méthode, mais il existe de meilleures façons de le faire, telles que les machines d'état et le yieldmot clé.

Les méthodes sont petites - pas un problème. - Les méthodes doivent être petites.

Les méthodes sont chacune consommées par une variété de types non liés - pas un problème. Vous avez créé une méthode générale qui peut être réutilisée dans de nombreux endroits non liés.

Une classe statique est utilisée uniquement comme un espace de noms - pas un problème. C'est ainsi que les méthodes statiques sont utilisées. Si ce style d'appels de méthodes vous dérange, essayez d'utiliser des méthodes d'extension ou la using staticdéclaration.

L'identifiant de classe statique n'a pratiquement aucun sens - problème . Cela n'a pas de sens car les développeurs y mettent des méthodes indépendantes. Mettez des méthodes non liées dans leurs propres classes statiques et donnez-leur des noms spécifiques et compréhensibles.

Lorsqu'un nouveau GLUM est ajouté, soit (a) cette classe "bag" est touchée (tristesse SRP) - pas un problème si vous suivez l'idée que seules les méthodes logiquement liées devraient être dans une classe statique. SRPn'est pas violé ici, car le principe doit être appliqué aux classes ou aux méthodes. La responsabilité unique des classes statiques signifie contenir différentes méthodes pour une "idée" générale. Cette idée peut être une fonctionnalité ou une conversion, ou des itérations (LINQ), ou la normalisation des données, ou la validation ...

ou moins fréquemment (b) une nouvelle classe "sac" est créée (plus de sacs) - pas de problème. Classes / classes statiques / fonctions - sont nos outils (développeurs) que nous utilisons pour concevoir des logiciels. Votre équipe a-t-elle des limites d'utilisation des cours? Quelles sont les limites maximales pour "plus de sacs"? La programmation est une question de contexte, si pour trouver une solution dans votre contexte particulier, vous vous retrouvez avec 200 classes statiques nommées de manière compréhensible, logiquement placées dans des espaces de noms / dossiers avec une hiérarchie logique - ce n'est pas un problème.

Le méta-nommage est inévitablement horrible, non standard et généralement incohérent en interne, que ce soit les Helpers, les utilitaires ou autre - problème. Fournissez de meilleurs noms qui décrivent le comportement attendu. Conservez uniquement les méthodes associées dans une classe, ce qui vous aide à mieux nommer.

Fabio
la source
Ce n'est pas une violation de SRP, mais plutôt une violation de principe ouvert / fermé. Cela dit, j'ai généralement trouvé que l'Opc posait plus de problèmes qu'il n'en valait
Paul
2
@Paul, le principe Open / Close ne peut pas être appliqué pour les classes statiques. C'était mon point, que les principes SRP ou OCP ne peuvent pas être appliqués pour les classes statiques. Vous pouvez l'appliquer pour les méthodes statiques.
Fabio
D'accord, il y avait une certaine confusion ici. La première liste d'éléments de la question n'est pas une liste de problèmes allégués. Il s'agit d'une liste des caractéristiques communes des méthodes spécifiques que j'examine. C'est le contexte pour aider les réponses à fournir des solutions plus applicables. Je vais modifier pour clarifier.
William
1
Concernant "la classe statique comme espace de noms", le fait qu'il s'agisse d'un modèle courant ne signifie pas que ce n'est pas un anti-modèle. La méthode (qui est fondamentalement une "fonction libre" pour emprunter un terme de C / C ++) au sein de cette classe statique particulière à faible cohésion est l'entité significative dans ce cas. Dans ce contexte particulier, il semble structurellement préférable d'avoir un sous-espace Ade noms avec 100 classes statiques à méthode unique (dans 100 fichiers) qu'une classe statique Aavec 100 méthodes dans un seul fichier.
William
1
J'ai fait un nettoyage assez important de votre réponse, principalement cosmétique. Je ne sais pas de quoi parle votre dernier paragraphe; personne ne s'inquiète de la façon dont ils testent le code qui appelle la Mathclasse statique.
Robert Harvey
5

Adoptez simplement la convention selon laquelle les classes statiques sont utilisées comme des espaces de noms dans des situations comme celle-ci en C #. Les espaces de noms obtiennent de bons noms, tout comme ces classes. La using staticfonctionnalité de C # 6 facilite également la vie.

Les noms de classe malodorants comme Helperssignalent que les méthodes doivent être divisées en classes statiques mieux nommées, si possible, mais l'une de vos hypothèses soulignées est que les méthodes avec lesquelles vous avez affaire sont "non liées par paires", ce qui implique une répartition en une nouvelle classe statique par méthode existante. C'est probablement bien, si

  • il y a de bons noms pour les nouvelles classes statiques et
  • vous le jugez bénéfique pour la maintenabilité de votre projet.

À titre d'exemple artificiel, Helpers.LoadImagepourrait devenir quelque chose comme FileSystemInteractions.LoadImage.

Pourtant, vous pourriez vous retrouver avec des sacs de méthode de classe statique. Voici comment cela peut se produire:

  • Peut-être que seulement certaines (ou aucune) des méthodes originales ont de bons noms pour leurs nouvelles classes.
  • Peut-être que les nouvelles classes évolueront plus tard pour inclure d'autres méthodes pertinentes.
  • Peut-être que le nom de la classe d'origine ne sentait pas mauvais et était en fait correct.

Il est important de se rappeler que ces sacs de méthode de classe statique ne sont pas très rares dans les vraies bases de code C #. Ce n'est probablement pas pathologique d'en avoir quelques petits .


Si vous voyez vraiment un avantage à avoir chaque méthode semblable à une "fonction libre" dans son propre fichier (ce qui est bien et utile si vous jugez honnêtement que cela profite réellement à la maintenabilité de votre projet), vous pourriez envisager de faire une telle classe statique au lieu d'une statique classe partielle, en utilisant le nom de classe statique comme vous le feriez pour un espace de noms, puis en le consommant via using static. Par exemple:

structure du projet factice utilisant ce modèle

Program.cs

namespace ConsoleApp1
{
    using static Foo;
    using static Flob;

    class Program
    {
        static void Main(string[] args)
        {
            Bar();
            Baz();
            Wibble();
            Wobble();
        }
    }
}

Foo / Bar.cs

namespace ConsoleApp1
{
    static partial class Foo
    {
        public static void Bar() { }
    }
}

Foo / Baz.cs

namespace ConsoleApp1
{
    static partial class Foo
    {
        public static void Baz() { }
    }
}

Flob / Wibble.cs

namespace ConsoleApp1
{
    static partial class Flob
    {
        public static void Wibble() { }
    }
}

Flob / Wobble.cs

namespace ConsoleApp1
{
    static partial class Flob
    {
        public static void Wobble() { }
    }
}
William
la source
-6

En règle générale, vous ne devriez pas avoir ou avoir besoin de "méthodes d'utilité générale". Dans votre logique métier. Jetez un autre coup d'œil et placez-les dans un endroit approprié.

Si vous écrivez une bibliothèque de mathématiques ou quelque chose comme ça, je suggère, bien que je les déteste normalement, des méthodes d'extension.

Vous pouvez utiliser des méthodes d'extension pour ajouter des fonctions aux types de données standard.

Par exemple, disons que j'ai une fonction générale MySort () au lieu de Helpers.MySort ou en ajoutant un nouveau ListOfMyThings.MySort je peux l'ajouter à IEnumerable

Ewan
la source
Le point est de ne pas prendre encore un autre "regard dur". Le point est d'avoir un modèle réutilisable qui peut facilement nettoyer un "sac" d'utilitaires. Je sais que les services publics sont des odeurs. La question le dit. L'objectif est d'isoler de manière sensible ces entités de domaine indépendantes (mais trop proches) pour l'instant et d'aller aussi facilement que possible sur le budget du projet.
William
1
Si vous n'avez pas de «méthodes d'utilité générale», vous n'isolez probablement pas assez les éléments de logique du reste de votre logique. Par exemple, pour autant que je sache, il n'y a pas de fonction de jointure de chemin URL à usage général dans .NET, donc je finis souvent par en écrire une. Ce genre de cruauté ferait mieux de faire partie de la bibliothèque standard, mais chaque bibliothèque standard manque quelque chose . L'alternative, en laissant la logique répétée directement à l'intérieur de votre autre code, le rend beaucoup moins lisible.
jpmc26
1
@Ewan ce n'est pas une fonction, c'est une signature de délégué à usage général ...
TheCatWhisperer
1
@Ewan C'était tout l'intérêt de TheCatWhisperer: les classes d'utilité sont une contrainte de devoir mettre des méthodes dans une classe. Content que vous soyez d'accord.
jpmc26