web-dev-qa-db-fra.com

Quand les énumérations ne sont-elles PAS une odeur de code?

dilemme

J'ai lu beaucoup de livres de bonnes pratiques sur les pratiques orientées objet, et presque tous les livres que j'ai lus avaient une partie où ils disent que les énumérations sont une odeur de code. Je pense qu'ils ont raté la partie où ils expliquent quand les énumérations sont valides.

En tant que tel, je recherche des lignes directrices et/ou des cas d'utilisation où les énumérations sont PAS une odeur de code et en fait une construction valide.

Sources:

"AVERTISSEMENT En règle générale, les énumérations sont des odeurs de code et doivent être refactorisées en classes polymorphes. [8]" Seemann, Mark, Dependency Injection in .Net, 2011, p. 342

[8] Martin Fowler et al., Refactoring: Improving the Design of Existing Code (New York: Addison-Wesley, 1999), 82.

Contexte

La cause de mon dilemme est une API de trading. Ils me donnent un flux de données Tick en envoyant via cette méthode:

void TickPrice(TickType tickType, double value)

enum TickType { BuyPrice, BuyQuantity, LastPrice, LastQuantity, ... }

J'ai essayé de créer un wrapper autour de cette API, car la rupture des changements est le mode de vie de cette API. Je voulais garder une trace de la valeur de chaque dernier type de tick reçu sur mon wrapper et je l'ai fait en utilisant un Dictionary of ticktypes:

Dictionary<TickType,double> LastValues

Pour moi, cela semblait être une bonne utilisation d'une énumération si elles sont utilisées comme clés. Mais j'ai des doutes parce que j'ai un endroit où je prends une décision basée sur cette collection et je ne peux pas penser à un moyen d'éliminer la déclaration de changement, je pourrais utiliser une usine mais cette usine aura toujours un basculer la déclaration quelque part. Il me semblait que je ne fais que bouger mais ça sent encore.

Il est facile de trouver les NE PAS faire des énumérations, mais les DO, pas si faciles, et j'apprécierais que les gens puissent partager leur expertise, les avantages et les inconvénients.

Réflexions

Certaines décisions et actions sont basées sur ces TickType et je n'arrive pas à penser à un moyen d'éliminer les instructions enum/switch. La solution la plus propre à laquelle je peux penser est d'utiliser une usine et de renvoyer une implémentation basée sur TickType. Même alors, j'aurai toujours une instruction switch qui renvoie une implémentation d'une interface.

La liste ci-dessous est l'un des exemples de classes où j'ai des doutes quant à l'utilisation incorrecte d'une énumération:

public class ExecutionSimulator
{
  Dictionary<TickType, double> LastReceived;
  void ProcessTick(TickType tickType, double value)
  {
    //Store Last Received TickType value
    LastReceived[tickType] = value;

    //Perform Order matching only on specific TickTypes
    switch(tickType)
    {
      case BidPrice:
      case BidSize:
        MatchSellOrders();
        break;
      case AskPrice:
      case AskSize:
        MatchBuyOrders();
        break;
    }       
  }
}
17
stromms

Les énumérations sont destinées aux cas d'utilisation lorsque vous avez littéralement énuméré toutes les valeurs possibles qu'une variable pourrait prendre. Déjà. Pensez à des cas d'utilisation comme les jours de la semaine ou les mois de l'année ou les valeurs de configuration d'un registre matériel. Des choses à la fois très stables et représentables par une simple valeur.

Gardez à l'esprit que si vous créez une couche anti-corruption, vous ne pouvez pas éviter d'avoir une instruction switch quelque part, en raison de la conception que vous enveloppez, mais si vous le faites correctement, vous pouvez le limiter à cet endroit et utiliser le polymorphisme ailleurs.

32
Karl Bielefeldt

Premièrement, une odeur de code ne signifie pas que quelque chose ne va pas. Cela signifie que quelque chose ne va pas. enum sent parce que c'est souvent abusé, mais cela ne signifie pas que vous devez les éviter. Il vous suffit de taper enum, d'arrêter et de rechercher de meilleures solutions.

Le cas particulier qui se produit le plus souvent est celui où les différentes énumérations correspondent à différents types avec des comportements différents mais la même interface. Par exemple, parler à différents backends, rendre différentes pages, etc. Ceux-ci sont beaucoup plus naturellement implémentés à l'aide de classes polymorphes.

Dans votre cas, le TickType ne correspond pas à des comportements différents. Ce sont différents types d'événements ou différentes propriétés de l'état actuel. Je pense donc que c'est l'endroit idéal pour une énumération.

17
Winston Ewert

Lors de la transmission des énumérations de données, aucune odeur de code

À mon humble avis, lors de la transmission de données à l'aide de enums pour indiquer qu'un champ peut avoir une valeur à partir d'un ensemble restreint (changeant rarement) de valeurs est bon.

Je considère préférable de transmettre des strings ou ints arbitraires. Les chaînes peuvent provoquer des problèmes par des variations d'orthographe et de capitalisation. Les entiers permettent de transmettre des valeurs hors plage et ont peu de sémantique (par exemple, je reçois 3 de votre service de trading, qu'est-ce que cela signifie? LastPrice? LastQuantity? Autre chose?

La transmission d'objets et l'utilisation de hiérarchies de classes n'est pas toujours possible; par exemple wcf ne permet pas à l'extrémité réceptrice de distinguer quelle classe a été envoyée.

Dans mon projet, le service utilise une hiérarchie de classes pour les effets des opérations, juste avant de transmettre via un DataContract l'objet de la hiérarchie des classes est copié dans un objet de type union qui contient un enum pour indiquer le type. Le client reçoit le DataContract et crée un objet d'une classe dans une hiérarchie en utilisant la valeur enum à switch et crée un objet du type correct.

Une autre raison pour laquelle on ne voudrait pas transmettre des objets de classe est que le service peut nécessiter un comportement complètement différent pour un objet transmis (tel que LastPrice) que le client. Dans ce cas, l'envoi de la classe et de ses méthodes n'est pas souhaité.

Les instructions switch sont-elles mauvaises?

À mon humble avis, une seule instruction switch- qui appelle différents constructeurs en fonction d'un enum n'est pas une odeur de code. Elle n'est pas nécessairement meilleure ou pire que d'autres méthodes, telles que la réflexion basée sur un nom de type; cela dépend de la situation réelle.

Avoir des commutateurs sur un enum partout est une odeur de code, oop fournit des alternatives qui sont souvent meilleures:

  • Utiliser un objet de différentes classes d'une hiérarchie qui ont des méthodes remplacées; c'est-à-dire le polymorphisme.
  • Utilisez le modèle de visiteur lorsque les classes de la hiérarchie changent rarement et lorsque les (nombreuses) opérations doivent être couplées de manière lâche aux classes de la hiérarchie.
8

et si vous optiez pour un type plus complexe:

    abstract class TickType
    {
      public abstract string Name {get;}
      public abstract double TickValue {get;}
    }

    class BuyPrice : TickType
    {
      public override string Name { get { return "Buy Price"; } }
      public override double TickValue { get { return 2.35d; } }
    }

    class BuyQuantity : TickType
    {
      public override string Name { get { return "Buy Quantity"; } }
      public override double TickValue { get { return 4.55d; } }
    }
//etcetera

alors vous pouvez charger vos types à partir de la réflexion ou le construire vous-même, mais la principale chose à faire ici est que vous maintenez le principe Open Close de SOLID

2
Chris

TL; DR

Pour répondre à la question, j'ai maintenant du mal à penser à un moment où les énumérations sont pas une odeur de code à un certain niveau. Il y a une certaine intention qu'ils déclarent efficacement (il y a un nombre clairement limité et limité de possibilités pour cette valeur), mais leur nature intrinsèquement fermée les rend architecturalement inférieurs.

Excusez-moi alors que je refaçonne des tonnes de mon ancien code./soupir; ^ D


Mais pourquoi?

Très bonne critique pourquoi c'est le cas de LosTechies ici :

// calculate the service fee
public double CalculateServiceFeeUsingEnum(Account acct)
{
    double totalFee = 0;
    foreach (var service in acct.ServiceEnums) { 

        switch (service)
        {
            case ServiceTypeEnum.ServiceA:
                totalFee += acct.NumOfUsers * 5;
                break;
            case ServiceTypeEnum.ServiceB:
                totalFee += 10;
                break;
        }
    }
    return totalFee;
} 

Cela a tous les mêmes problèmes que le code ci-dessus. À mesure que l'application s'agrandit, les chances d'avoir des instructions de branche similaires vont augmenter. De plus, au fur et à mesure que vous déployez des services premium, vous devrez continuellement modifier ce code, ce qui viole le principe ouvert-fermé. Il y a aussi d'autres problèmes ici. La fonction de calcul des frais de service ne devrait pas avoir besoin de connaître les montants réels de chaque service. Ce sont des informations qui doivent être encapsulées.

Un petit côté: les énumérations sont une structure de données très limitée. Si vous n'utilisez pas une énumération pour ce qu'elle est réellement, un entier étiqueté, vous avez besoin d'une classe pour modéliser correctement l'abstraction correctement. Vous pouvez utiliser la classe d'énumération impressionnante de Jimmy pour utiliser des classes afin de les utiliser également comme étiquettes.

Refactorisons ceci pour utiliser un comportement polymorphe. Ce dont nous avons besoin, c'est d'une abstraction qui nous permettra de contenir le comportement nécessaire pour calculer les frais pour un service.

public interface ICalculateServiceFee
{
    double CalculateServiceFee(Account acct);
}

...

Nous pouvons maintenant créer nos implémentations concrètes de l'interface et les attacher [au] compte.

public class Account{
    public int NumOfUsers{get;set;}
    public ICalculateServiceFee[] Services { get; set; }
} 

public class ServiceA : ICalculateServiceFee
{
    double feePerUser = 5; 

    public double CalculateServiceFee(Account acct)
    {
        return acct.NumOfUsers * feePerUser;
    }
} 

public class ServiceB : ICalculateServiceFee
{
    double serviceFee = 10;
    public double CalculateServiceFee(Account acct)
    {
        return serviceFee;
    }
} 

Un autre cas d'implémentation ...

En fin de compte, si vous avez un comportement qui dépend des valeurs d'énumération, pourquoi ne pas avoir à la place différentes implémentations d'une interface ou d'une classe parent similaire qui garantit que cette valeur existe? Dans mon cas, je regarde différents messages d'erreur basés sur REST codes d'état. Au lieu de ...

private static string _getErrorCKey(int statusCode)
{
    string ret;

    switch (statusCode)
    {
        case StatusCodes.Status403Forbidden:
            ret = "BRANCH_UNAUTHORIZED";
            break;

        case StatusCodes.Status422UnprocessableEntity:
            ret = "BRANCH_NOT_FOUND";
            break;

        default:
            ret = "BRANCH_GENERIC_ERROR";
            break;
    }

    return ret;
}

... je devrais peut-être envelopper les codes de statut dans les classes.

public interface IAmStatusResult
{
    int StatusResult { get; }    // Pretend an int's okay for now.
    string ErrorKey { get; }
}

Ensuite, chaque fois que j'ai besoin d'un nouveau type d'IAmStatusResult, je le code ...

public class UnauthorizedBranchStatusResult : IAmStatusResult
{
    public int StatusResult => 403;
    public string ErrorKey => "BRANCH_UNAUTHORIZED";
}

... et maintenant je peux m'assurer que le code antérieur se rend compte qu'il a un IAmStatusResult dans la portée et référence son entity.ErrorKey au lieu de la plus compliquée et morte _getErrorCode(403).

Et, plus important encore, chaque fois que j'ajoute un nouveau type de valeur de retour, aucun autre code ne doit être ajouté pour le gérer. Whaddya sait, les enum et switch étaient probablement des odeurs de code.

Profit.

1
ruffin

Que l'utilisation d'une énumération soit ou non une odeur de code dépend du contexte. Je pense que vous pouvez trouver des idées pour répondre à votre question si vous considérez le problème d'expression . Donc, vous avez une collection de différents types et une collection d'opérations sur eux, et vous devez organiser votre code. Il existe deux options simples:

  • Organisez le code selon les opérations. Dans ce cas, vous pouvez utiliser une énumération pour baliser différents types et avoir une instruction switch dans chaque procédure qui utilise les données balisées.
  • Organisez le code selon les types de données. Dans ce cas, vous pouvez remplacer l'énumération par une interface et utiliser une classe pour chaque élément de l'énumération. Vous implémentez ensuite chaque opération en tant que méthode dans chaque classe.

Quelle solution est la meilleure?

Si, comme Karl Bielefeldt l'a souligné, vos types sont fixes et que vous vous attendez à ce que le système se développe principalement en ajoutant de nouvelles opérations sur ces types, alors utiliser une énumération et avoir une instruction switch est une meilleure solution: chaque fois que vous avez besoin d'une nouvelle opération, vous implémentez simplement une nouvelle procédure alors qu'en utilisant des classes, vous devrez ajouter une méthode à chaque classe.

D'un autre côté, si vous vous attendez à avoir un ensemble d'opérations plutôt stable mais que vous pensez que vous devrez ajouter plus de types de données au fil du temps, l'utilisation d'une solution orientée objet est plus pratique: comme de nouveaux types de données doivent être implémentés, vous venez continuez à ajouter de nouvelles classes implémentant la même interface, alors que si vous utilisez une énumération, vous devrez mettre à jour toutes les instructions switch dans toutes les procédures utilisant l'énumération.

Si vous ne pouvez pas classer votre problème dans l'une des deux options ci-dessus, vous pouvez rechercher des solutions plus sophistiquées (voir par exemple à nouveau la page Wikipedia cité ci-dessus pour une brève discussion et des références pour une lecture plus approfondie).

Vous devez donc essayer de comprendre dans quelle direction votre application peut évoluer, puis choisir une solution appropriée.

Étant donné que les livres auxquels vous vous référez traitent du paradigme orienté objet, il n'est pas surprenant qu'ils soient biaisés contre l'utilisation d'énumérations. Cependant, une solution d'orientation d'objet n'est pas toujours la meilleure option.

Conclusion: les énumérations ne sont pas nécessairement une odeur de code.

0
Giorgio